@vectoriox/iox-ui 5.54.0 → 5.55.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 +849 -44
- package/fesm2022/vectoriox-iox-ui-generation-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui-harness-core.mjs +450 -30
- package/fesm2022/vectoriox-iox-ui-harness-core.mjs.map +1 -1
- package/generation-core/index.d.ts +302 -2
- package/harness-core/index.d.ts +297 -16
- package/package.json +1 -1
- package/src/generation-core/ai-run.ts +17 -2
- package/src/generation-core/json-schema.ts +97 -0
- package/src/generation-core/plan-edits.ts +428 -0
- package/src/generation-core/plan-words.ts +114 -0
- package/src/generation-core/public-api.ts +4 -0
- package/src/generation-core/site-plan-schema.ts +239 -0
- package/src/harness-core/generation-view.ts +115 -0
- package/src/harness-core/palette.ts +198 -0
- package/src/harness-core/public-api.ts +4 -0
- package/src/harness-core/reference-colour.ts +45 -0
- package/src/harness-core/site-analysis.ts +8 -29
- package/src/harness-core/site-reading.ts +214 -0
|
@@ -7567,7 +7567,7 @@ var SiteTemplateId;
|
|
|
7567
7567
|
const STUDIO_TEMPLATE = SITE_TEMPLATES[SiteTemplateId.Studio];
|
|
7568
7568
|
class SiteTemplateError extends Error {
|
|
7569
7569
|
}
|
|
7570
|
-
const clone$
|
|
7570
|
+
const clone$4 = (value) => JSON.parse(JSON.stringify(value));
|
|
7571
7571
|
/**
|
|
7572
7572
|
* The template as a Site Plan (`source.kind: template`), with the customer's business and look. A
|
|
7573
7573
|
* required page cannot be left out, and an unknown key is refused — nothing is guessed.
|
|
@@ -7584,12 +7584,12 @@ function planFromTemplate(template, input) {
|
|
|
7584
7584
|
return {
|
|
7585
7585
|
version: SITE_PLAN_VERSION,
|
|
7586
7586
|
source: { kind: SitePlanSourceKind.Template, templateId: template.id, templateVersion: template.version },
|
|
7587
|
-
business: clone$
|
|
7588
|
-
design: clone$
|
|
7589
|
-
contentTypes: clone$
|
|
7590
|
-
dataSources: clone$
|
|
7591
|
-
pages: clone$
|
|
7592
|
-
globals: clone$
|
|
7587
|
+
business: clone$4(input.business),
|
|
7588
|
+
design: clone$4(input.design),
|
|
7589
|
+
contentTypes: clone$4(template.contentTypes),
|
|
7590
|
+
dataSources: clone$4(template.dataSources),
|
|
7591
|
+
pages: clone$4(template.pages.filter(p => !omit.has(p.key))),
|
|
7592
|
+
globals: clone$4(template.globals),
|
|
7593
7593
|
};
|
|
7594
7594
|
}
|
|
7595
7595
|
|
|
@@ -7598,7 +7598,7 @@ function planFromTemplate(template, input) {
|
|
|
7598
7598
|
* wrong — never something to work around. */
|
|
7599
7599
|
class ComposeError extends Error {
|
|
7600
7600
|
}
|
|
7601
|
-
const clone$
|
|
7601
|
+
const clone$3 = (value) => JSON.parse(JSON.stringify(value));
|
|
7602
7602
|
/**
|
|
7603
7603
|
* A NEW node of `type`, as the builder creates one (ComponentRegistryService.createConfig, serialized):
|
|
7604
7604
|
* the type's constructor defaults, its text colour released to `inherit` (NEW_NODE_STYLE_DEFAULTS), then
|
|
@@ -7613,8 +7613,8 @@ function createSerializedNode(id, type, props = {}, elementStyle, options = {})
|
|
|
7613
7613
|
const node = {
|
|
7614
7614
|
id,
|
|
7615
7615
|
type,
|
|
7616
|
-
props: { ...clone$
|
|
7617
|
-
styleProps: clone$
|
|
7616
|
+
props: { ...clone$3(baseline.props), ...props },
|
|
7617
|
+
styleProps: clone$3(baseline.styleProps),
|
|
7618
7618
|
bindings: [],
|
|
7619
7619
|
interactions: [],
|
|
7620
7620
|
};
|
|
@@ -7665,16 +7665,16 @@ function walkSerialized(nodes, visit, ancestors = []) {
|
|
|
7665
7665
|
* own field names) whose key lists the gate READS — so the shape a model is told about is the shape refused —
|
|
7666
7666
|
* and a collector that records each refusal at its dotted path. Generic over each gate's issue-code enum.
|
|
7667
7667
|
*/
|
|
7668
|
-
const OPTIONAL_MARK = '?';
|
|
7668
|
+
const OPTIONAL_MARK$1 = '?';
|
|
7669
7669
|
const DYNAMIC_KEY = /^\[[^\]]+\]\??$/;
|
|
7670
7670
|
const isDynamic = (key) => DYNAMIC_KEY.test(key);
|
|
7671
7671
|
/** Every fixed key an object of the shape may carry. */
|
|
7672
7672
|
function shapeKeys(entry) {
|
|
7673
|
-
return Object.keys(entry).filter(k => !isDynamic(k)).map(k => (k.endsWith(OPTIONAL_MARK) ? k.slice(0, -1) : k));
|
|
7673
|
+
return Object.keys(entry).filter(k => !isDynamic(k)).map(k => (k.endsWith(OPTIONAL_MARK$1) ? k.slice(0, -1) : k));
|
|
7674
7674
|
}
|
|
7675
7675
|
/** The keys an object of the shape must carry. */
|
|
7676
7676
|
function requiredShapeKeys(entry) {
|
|
7677
|
-
return Object.keys(entry).filter(k => !isDynamic(k) && !k.endsWith(OPTIONAL_MARK));
|
|
7677
|
+
return Object.keys(entry).filter(k => !isDynamic(k) && !k.endsWith(OPTIONAL_MARK$1));
|
|
7678
7678
|
}
|
|
7679
7679
|
const isPlainObject = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
7680
7680
|
const isNonEmptyString = (v) => typeof v === 'string' && v.trim() !== '';
|
|
@@ -7729,6 +7729,83 @@ class PathIssueCollector {
|
|
|
7729
7729
|
}
|
|
7730
7730
|
}
|
|
7731
7731
|
|
|
7732
|
+
const OPTIONAL_MARK = '?';
|
|
7733
|
+
class JsonSchemaShapeError extends Error {
|
|
7734
|
+
}
|
|
7735
|
+
const jsonText = () => ({ type: 'string' });
|
|
7736
|
+
const jsonBoolean = () => ({ type: 'boolean' });
|
|
7737
|
+
const jsonInteger = () => ({ type: 'integer' });
|
|
7738
|
+
const jsonOneOf = (values) => ({ type: 'string', enum: [...values] });
|
|
7739
|
+
const jsonListOf = (items) => ({ type: 'array', items });
|
|
7740
|
+
const jsonAnyOf = (options) => ({ anyOf: [...options] });
|
|
7741
|
+
/** A closed object with exactly these properties. */
|
|
7742
|
+
function jsonClosedObject(properties, required, description) {
|
|
7743
|
+
return {
|
|
7744
|
+
type: 'object',
|
|
7745
|
+
properties,
|
|
7746
|
+
required: [...required],
|
|
7747
|
+
additionalProperties: false,
|
|
7748
|
+
...(description ? { description } : {}),
|
|
7749
|
+
};
|
|
7750
|
+
}
|
|
7751
|
+
/** The table's own words for a key (`key` or `key?`). */
|
|
7752
|
+
function describeShapeKey(entry, key) {
|
|
7753
|
+
return String(entry[key] ?? entry[`${key}${OPTIONAL_MARK}`] ?? '');
|
|
7754
|
+
}
|
|
7755
|
+
/**
|
|
7756
|
+
* An object of one shape-table entry: exactly its fixed keys, its required keys required, each described by the
|
|
7757
|
+
* table; an entry's `[name]` keys are not object keys — `dynamic` writes them as one list. Throws when the properties
|
|
7758
|
+
* given are not the table's keys.
|
|
7759
|
+
*/
|
|
7760
|
+
function jsonShapedObject(entry, properties, name = 'shape', dynamic) {
|
|
7761
|
+
const expected = [...shapeKeys(entry)].sort();
|
|
7762
|
+
const given = Object.keys(properties).sort();
|
|
7763
|
+
if (expected.join() !== given.join()) {
|
|
7764
|
+
throw new JsonSchemaShapeError(`${name}: the format's keys (${given.join(', ')}) are not the shape table's (${expected.join(', ')})`);
|
|
7765
|
+
}
|
|
7766
|
+
const dynamicKey = Object.keys(entry).find(k => /^\[[^\]]+\]\??$/.test(k));
|
|
7767
|
+
if (!!dynamic !== !!dynamicKey)
|
|
7768
|
+
throw new JsonSchemaShapeError(`${name}: ${dynamicKey ? `its ${dynamicKey} keys need a list on the wire` : 'it has no [name] keys to list'}`);
|
|
7769
|
+
const described = Object.fromEntries(Object.entries(properties).map(([key, schema]) => [key, { ...schema, description: describeShapeKey(entry, key) }]));
|
|
7770
|
+
if (dynamic && dynamicKey)
|
|
7771
|
+
described[dynamic.key] = { ...dynamic.schema, description: `${dynamicKey}: ${entry[dynamicKey]}` };
|
|
7772
|
+
return jsonClosedObject(described, requiredShapeKeys(entry));
|
|
7773
|
+
}
|
|
7774
|
+
/** Every keyword a schema uses — what a spec checks against the strict subset. */
|
|
7775
|
+
function jsonSchemaKeywords(node, out = new Set()) {
|
|
7776
|
+
if (Array.isArray(node))
|
|
7777
|
+
node.forEach(n => jsonSchemaKeywords(n, out));
|
|
7778
|
+
else if (node && typeof node === 'object') {
|
|
7779
|
+
for (const [k, v] of Object.entries(node)) {
|
|
7780
|
+
out.add(k);
|
|
7781
|
+
if (k === 'properties')
|
|
7782
|
+
Object.values(v).forEach(p => jsonSchemaKeywords(p, out));
|
|
7783
|
+
else if (k !== 'enum' && k !== 'required' && k !== 'description')
|
|
7784
|
+
jsonSchemaKeywords(v, out);
|
|
7785
|
+
}
|
|
7786
|
+
}
|
|
7787
|
+
return out;
|
|
7788
|
+
}
|
|
7789
|
+
/** The keywords the strict subset never uses. */
|
|
7790
|
+
const JSON_SCHEMA_BANNED_KEYWORDS = [
|
|
7791
|
+
'minLength', 'maxLength', 'minimum', 'maximum', 'minItems', 'maxItems', 'pattern', 'oneOf', '$ref', 'const', 'format',
|
|
7792
|
+
];
|
|
7793
|
+
/** Every object node of a schema (through properties, items and anyOf). */
|
|
7794
|
+
function jsonSchemaObjects(node, out = []) {
|
|
7795
|
+
if (!node || typeof node !== 'object')
|
|
7796
|
+
return out;
|
|
7797
|
+
const n = node;
|
|
7798
|
+
if (n['type'] === 'object')
|
|
7799
|
+
out.push(n);
|
|
7800
|
+
for (const p of Object.values(n['properties'] ?? {}))
|
|
7801
|
+
jsonSchemaObjects(p, out);
|
|
7802
|
+
if (n['items'])
|
|
7803
|
+
jsonSchemaObjects(n['items'], out);
|
|
7804
|
+
for (const option of n['anyOf'] ?? [])
|
|
7805
|
+
jsonSchemaObjects(option, out);
|
|
7806
|
+
return out;
|
|
7807
|
+
}
|
|
7808
|
+
|
|
7732
7809
|
/**
|
|
7733
7810
|
* The Site Plan validator — the gate in front of the composer. The wizard's plan and (later) the AI's
|
|
7734
7811
|
* plan go through the SAME checks: an unknown pattern, a slot the pattern does not declare, a source
|
|
@@ -7776,20 +7853,20 @@ const RESERVED_PREFIX = '$';
|
|
|
7776
7853
|
const ID_FIELD = '_id';
|
|
7777
7854
|
/** The documented plan shape (contract `site.planShape`): its key lists are READ (`shape-check`), so a key
|
|
7778
7855
|
* the plan documents is a key the gate validates — and an undocumented one is refused. */
|
|
7779
|
-
const SHAPE = SPEC_SITE_PLAN_SHAPE;
|
|
7780
|
-
const PLAN_KEYS = shapeKeys(SHAPE['plan']);
|
|
7781
|
-
const TEMPLATE_SOURCE_KEYS = shapeKeys(SHAPE['sourceTemplate']);
|
|
7782
|
-
const AI_SOURCE_KEYS = shapeKeys(SHAPE['sourceAi']);
|
|
7783
|
-
const BUSINESS_KEYS = shapeKeys(SHAPE['business']);
|
|
7784
|
-
const DESIGN_REQUIRED = requiredShapeKeys(SHAPE['design']);
|
|
7785
|
-
const DESIGN_KEYS = shapeKeys(SHAPE['design']);
|
|
7786
|
-
const CONTENT_TYPE_KEYS = shapeKeys(SHAPE['contentType']);
|
|
7787
|
-
const FIELD_KEYS = shapeKeys(SHAPE['field']);
|
|
7788
|
-
const SEED_FIXED_KEYS = shapeKeys(SHAPE['seed']);
|
|
7789
|
-
const CMS_SOURCE_KEYS = shapeKeys(SHAPE['cmsDataSource']);
|
|
7790
|
-
const RELATIVE_SOURCE_KEYS = shapeKeys(SHAPE['relativeDataSource']);
|
|
7791
|
-
const PAGE_KEYS = shapeKeys(SHAPE['page']);
|
|
7792
|
-
const SECTION_KEYS = shapeKeys(SHAPE['section']);
|
|
7856
|
+
const SHAPE$1 = SPEC_SITE_PLAN_SHAPE;
|
|
7857
|
+
const PLAN_KEYS = shapeKeys(SHAPE$1['plan']);
|
|
7858
|
+
const TEMPLATE_SOURCE_KEYS = shapeKeys(SHAPE$1['sourceTemplate']);
|
|
7859
|
+
const AI_SOURCE_KEYS = shapeKeys(SHAPE$1['sourceAi']);
|
|
7860
|
+
const BUSINESS_KEYS = shapeKeys(SHAPE$1['business']);
|
|
7861
|
+
const DESIGN_REQUIRED = requiredShapeKeys(SHAPE$1['design']);
|
|
7862
|
+
const DESIGN_KEYS = shapeKeys(SHAPE$1['design']);
|
|
7863
|
+
const CONTENT_TYPE_KEYS = shapeKeys(SHAPE$1['contentType']);
|
|
7864
|
+
const FIELD_KEYS = shapeKeys(SHAPE$1['field']);
|
|
7865
|
+
const SEED_FIXED_KEYS = shapeKeys(SHAPE$1['seed']);
|
|
7866
|
+
const CMS_SOURCE_KEYS = shapeKeys(SHAPE$1['cmsDataSource']);
|
|
7867
|
+
const RELATIVE_SOURCE_KEYS = shapeKeys(SHAPE$1['relativeDataSource']);
|
|
7868
|
+
const PAGE_KEYS = shapeKeys(SHAPE$1['page']);
|
|
7869
|
+
const SECTION_KEYS = shapeKeys(SHAPE$1['section']);
|
|
7793
7870
|
const GLOBAL_POSITION = {
|
|
7794
7871
|
[GlobalSlot.Header]: PatternGlobalPosition.Before,
|
|
7795
7872
|
[GlobalSlot.Footer]: PatternGlobalPosition.After,
|
|
@@ -7801,11 +7878,11 @@ const has = (table, key) => Object.prototype.hasOwnProperty.call(table, key);
|
|
|
7801
7878
|
function routeParams(route) {
|
|
7802
7879
|
return [...route.matchAll(ROUTE_PARAM$1)].map(m => m[1]);
|
|
7803
7880
|
}
|
|
7804
|
-
class Collector extends PathIssueCollector {
|
|
7881
|
+
let Collector$1 = class Collector extends PathIssueCollector {
|
|
7805
7882
|
constructor() {
|
|
7806
7883
|
super('plan', { shape: SitePlanIssueCode.Shape, unknownKey: SitePlanIssueCode.UnknownKey, duplicate: SitePlanIssueCode.Duplicate });
|
|
7807
7884
|
}
|
|
7808
|
-
}
|
|
7885
|
+
};
|
|
7809
7886
|
function validateSource(c, source) {
|
|
7810
7887
|
if (!isPlainObject(source))
|
|
7811
7888
|
return c.add(SitePlanIssueCode.Shape, 'source', 'must be an object');
|
|
@@ -8143,7 +8220,7 @@ function validateGlobals(c, globals) {
|
|
|
8143
8220
|
}
|
|
8144
8221
|
/** Check a Site Plan against the plan shape and the pattern library. Never throws, never repairs. */
|
|
8145
8222
|
function validateSitePlan(plan) {
|
|
8146
|
-
const c = new Collector();
|
|
8223
|
+
const c = new Collector$1();
|
|
8147
8224
|
if (!isPlainObject(plan)) {
|
|
8148
8225
|
c.add(SitePlanIssueCode.Shape, '', 'a site plan is an object');
|
|
8149
8226
|
return { valid: false, issues: c.issues };
|
|
@@ -8506,7 +8583,7 @@ function bar(kit, root, pages, shape, count, drawerId, at) {
|
|
|
8506
8583
|
return b;
|
|
8507
8584
|
}
|
|
8508
8585
|
// ── the edit ────────────────────────────────────────────────────────────────────────────────
|
|
8509
|
-
const clone$
|
|
8586
|
+
const clone$2 = (v) => JSON.parse(JSON.stringify(v));
|
|
8510
8587
|
function removeRole(node, role) {
|
|
8511
8588
|
let removed = null;
|
|
8512
8589
|
const visit = (n) => {
|
|
@@ -8558,7 +8635,7 @@ function clampBarCount(n, shape) {
|
|
|
8558
8635
|
function planNavShape(globals, shape, options = {}, navId) {
|
|
8559
8636
|
if (!Object.values(NavShape).includes(shape))
|
|
8560
8637
|
throw new NavShapeError(`unknown phone menu shape "${shape}"`);
|
|
8561
|
-
const out = clone$
|
|
8638
|
+
const out = clone$2(globals);
|
|
8562
8639
|
const root = findNavRoot(out, navId);
|
|
8563
8640
|
if (!root)
|
|
8564
8641
|
throw new NavShapeError('no Navigation bar among the globals');
|
|
@@ -8678,7 +8755,7 @@ function nameParts(root) {
|
|
|
8678
8755
|
* bar does not link to yet (Home when every page is there). Refused at five (Q2). Input not mutated.
|
|
8679
8756
|
*/
|
|
8680
8757
|
function addBarItem(globals, options = {}) {
|
|
8681
|
-
const out = clone$
|
|
8758
|
+
const out = clone$2(globals);
|
|
8682
8759
|
const root = findNavRoot(out);
|
|
8683
8760
|
const b = find(out, NavRole.Bar);
|
|
8684
8761
|
if (!root || !b)
|
|
@@ -8698,7 +8775,7 @@ function addBarItem(globals, options = {}) {
|
|
|
8698
8775
|
return { globals: out, added: true };
|
|
8699
8776
|
}
|
|
8700
8777
|
function barOf(globals) {
|
|
8701
|
-
const out = clone$
|
|
8778
|
+
const out = clone$2(globals);
|
|
8702
8779
|
const b = find(out, NavRole.Bar);
|
|
8703
8780
|
return { out, kids: b ? (b.children ?? (b.children = [])) : null };
|
|
8704
8781
|
}
|
|
@@ -8839,7 +8916,7 @@ const COMPOSED = { releaseLayoutDefaults: true };
|
|
|
8839
8916
|
const TEXT_VARIABLE = /\{(year|business\.name)\}/g;
|
|
8840
8917
|
/** What `page.*` may read on a node stamped for a navigation page. */
|
|
8841
8918
|
const STAMPED_PAGE_FIELDS = new Set(['name', 'route']);
|
|
8842
|
-
const clone = (value) => JSON.parse(JSON.stringify(value));
|
|
8919
|
+
const clone$1 = (value) => JSON.parse(JSON.stringify(value));
|
|
8843
8920
|
/** 32-bit FNV-1a, base 36 — a short stable id fragment. */
|
|
8844
8921
|
function stableHash(text) {
|
|
8845
8922
|
let h = 0x811c9dc5;
|
|
@@ -9612,10 +9689,10 @@ function composeSite(plan, context) {
|
|
|
9612
9689
|
globals = planNavShape(globals, shape, {}, slot).globals;
|
|
9613
9690
|
}
|
|
9614
9691
|
return {
|
|
9615
|
-
source: clone(plan.source),
|
|
9616
|
-
business: clone(plan.business),
|
|
9617
|
-
design: resolveDesignChoices(clone(plan.design)),
|
|
9618
|
-
schemas: clone(plan.contentTypes),
|
|
9692
|
+
source: clone$1(plan.source),
|
|
9693
|
+
business: clone$1(plan.business),
|
|
9694
|
+
design: resolveDesignChoices(clone$1(plan.design)),
|
|
9695
|
+
schemas: clone$1(plan.contentTypes),
|
|
9619
9696
|
items: Object.fromEntries(plan.contentTypes.map(t => [t.name, composeItems(t)])),
|
|
9620
9697
|
dataSources: plan.dataSources.map(composeDataSource),
|
|
9621
9698
|
pages,
|
|
@@ -9731,13 +9808,13 @@ const ENTRANCE_TRIGGER = 'viewportEnter';
|
|
|
9731
9808
|
const ENTRANCE_EASING = 'ease-out';
|
|
9732
9809
|
const LEVELS = SPEC_AXES.animations;
|
|
9733
9810
|
const FAMILIES = SPEC_AXES.animationFamilies;
|
|
9734
|
-
const NOTHING = { content: null, parts: {} };
|
|
9811
|
+
const NOTHING$1 = { content: null, parts: {} };
|
|
9735
9812
|
/** What a section of `family` moves at `level` — nothing for an unknown level or family. */
|
|
9736
9813
|
function sectionEntrances(level, family) {
|
|
9737
9814
|
const lv = level ? LEVELS[level] : undefined;
|
|
9738
9815
|
const allow = FAMILIES[family] ?? [];
|
|
9739
9816
|
if (!lv || !allow.length)
|
|
9740
|
-
return NOTHING;
|
|
9817
|
+
return NOTHING$1;
|
|
9741
9818
|
if (lv.parts.content && allow.includes(EntrancePart.Content))
|
|
9742
9819
|
return { content: lv.parts.content, parts: {} };
|
|
9743
9820
|
const parts = {};
|
|
@@ -10009,6 +10086,14 @@ var AiStep;
|
|
|
10009
10086
|
AiStep["NameStructures"] = "scanNaming";
|
|
10010
10087
|
})(AiStep || (AiStep = {}));
|
|
10011
10088
|
const AI_STEPS = Object.values(AiStep);
|
|
10089
|
+
/** Each step's name on the wire (the control plane's `label`, the console) — the screens translate their own. */
|
|
10090
|
+
const AI_STEP_LABELS = {
|
|
10091
|
+
[AiStep.ReadMessage]: 'Read your message',
|
|
10092
|
+
[AiStep.PlanSite]: 'Plan the site',
|
|
10093
|
+
[AiStep.ChangePlan]: 'Change the plan',
|
|
10094
|
+
[AiStep.MakePictures]: 'Make pictures',
|
|
10095
|
+
[AiStep.NameStructures]: 'Name repeated structures',
|
|
10096
|
+
};
|
|
10012
10097
|
/** What a model must be able to do for a step to run on it. */
|
|
10013
10098
|
var AiCapability;
|
|
10014
10099
|
(function (AiCapability) {
|
|
@@ -10028,7 +10113,8 @@ const AI_STEP_TYPICAL_TOKENS = {
|
|
|
10028
10113
|
[AiStep.ReadMessage]: { input: 8_800, output: 1_500, cacheWrite: 0, cacheRead: 0 },
|
|
10029
10114
|
[AiStep.PlanSite]: { input: 12_000, output: 7_000, cacheWrite: 0, cacheRead: 0 },
|
|
10030
10115
|
[AiStep.ChangePlan]: { input: 6_000, output: 500, cacheWrite: 0, cacheRead: 0 },
|
|
10031
|
-
|
|
10116
|
+
// a 1 K picture is 1,120 image tokens (Google's figure for Nano Banana 2) — I-D4
|
|
10117
|
+
[AiStep.MakePictures]: { input: 0, output: 0, cacheWrite: 0, cacheRead: 0, imageOutput: 1_120 },
|
|
10032
10118
|
[AiStep.NameStructures]: { input: 80_000, output: 10_000, cacheWrite: 0, cacheRead: 0 },
|
|
10033
10119
|
};
|
|
10034
10120
|
/** 1 credit = 1 US cent of what the AI actually cost (decision D4). */
|
|
@@ -10042,7 +10128,8 @@ function costUsd(tokens, price) {
|
|
|
10042
10128
|
return tokens.input * perToken(price.input, 0)
|
|
10043
10129
|
+ tokens.output * perToken(price.output, 0)
|
|
10044
10130
|
+ tokens.cacheWrite * perToken(price.cacheWrite, price.input)
|
|
10045
|
-
+ tokens.cacheRead * perToken(price.cacheRead, price.input)
|
|
10131
|
+
+ tokens.cacheRead * perToken(price.cacheRead, price.input)
|
|
10132
|
+
+ (tokens.imageOutput ?? 0) * perToken(price.imageOutput, price.output);
|
|
10046
10133
|
}
|
|
10047
10134
|
/** Dollars → credits, kept to a millionth of a credit (a short read costs a fraction of one). */
|
|
10048
10135
|
function creditsOfUsd(usd) {
|
|
@@ -10122,6 +10209,724 @@ var AiCallStatus;
|
|
|
10122
10209
|
AiCallStatus["Interrupted"] = "interrupted";
|
|
10123
10210
|
})(AiCallStatus || (AiCallStatus = {}));
|
|
10124
10211
|
|
|
10212
|
+
/**
|
|
10213
|
+
* The structured-output format a model writes a Site Plan in (AI plan §3d step 4, M1-3) — built from the SAME shape
|
|
10214
|
+
* table `validateSitePlan` reads (`SPEC_SITE_PLAN_SHAPE`) and the SAME pattern library, so the plan a model is told
|
|
10215
|
+
* about is the plan the gate refuses. ONE source: a key the table gains breaks this builder until it is written here.
|
|
10216
|
+
*
|
|
10217
|
+
* Two maps of the shape are written as LISTS on the wire, because no strict mode can say "any key": a section's
|
|
10218
|
+
* `slots` (`[{ slot, text }]`, the slot names closed per pattern) and a seed's field values (`values: [{ field,
|
|
10219
|
+
* value }]`). `sitePlanFromWire` turns them back; nothing else differs. Every section is one `anyOf` branch per
|
|
10220
|
+
* pattern — its id, its declared slots, its optional parts, a binding only when it takes one — and the design, the
|
|
10221
|
+
* source and the version are PINNED to the owner's pick and this run (one-value enums), so the provider cannot let
|
|
10222
|
+
* a model change the look. The gate still checks both (`plan.design` deep-equals the pick).
|
|
10223
|
+
*/
|
|
10224
|
+
const SITE_PLAN_SCHEMA_NAME = 'site_plan';
|
|
10225
|
+
/** The wire keys of the two maps written as lists. */
|
|
10226
|
+
var SitePlanWireKey;
|
|
10227
|
+
(function (SitePlanWireKey) {
|
|
10228
|
+
SitePlanWireKey["SlotName"] = "slot";
|
|
10229
|
+
SitePlanWireKey["SlotText"] = "text";
|
|
10230
|
+
SitePlanWireKey["SeedValues"] = "values";
|
|
10231
|
+
SitePlanWireKey["SeedField"] = "field";
|
|
10232
|
+
SitePlanWireKey["SeedValue"] = "value";
|
|
10233
|
+
})(SitePlanWireKey || (SitePlanWireKey = {}));
|
|
10234
|
+
const SHAPE = SPEC_SITE_PLAN_SHAPE;
|
|
10235
|
+
const one$1 = (value) => jsonOneOf([value]);
|
|
10236
|
+
const oneBoolean = (value) => ({ type: 'boolean', enum: [value] });
|
|
10237
|
+
/** The pick, key by key, as one-value enums; an optional key the pick leaves out may only be left out (or empty). */
|
|
10238
|
+
function pinnedDesign(design) {
|
|
10239
|
+
const entry = SHAPE['design'];
|
|
10240
|
+
const values = design;
|
|
10241
|
+
const properties = {};
|
|
10242
|
+
for (const key of Object.keys(entry).map(k => k.replace(/\?$/, ''))) {
|
|
10243
|
+
const v = values[key];
|
|
10244
|
+
if (typeof v === 'string')
|
|
10245
|
+
properties[key] = one$1(v);
|
|
10246
|
+
else if (typeof v === 'boolean')
|
|
10247
|
+
properties[key] = oneBoolean(v);
|
|
10248
|
+
else if (isPlainObject(v))
|
|
10249
|
+
properties[key] = jsonClosedObject(Object.fromEntries(Object.entries(v).map(([role, family]) => [role, one$1(String(family))])), Object.keys(v));
|
|
10250
|
+
else
|
|
10251
|
+
properties[key] = jsonClosedObject({}, []);
|
|
10252
|
+
}
|
|
10253
|
+
const shaped = jsonShapedObject(entry, properties, 'design');
|
|
10254
|
+
// Everything the pick carries is required: a model may not leave a choice out and let a default stand in.
|
|
10255
|
+
const given = Object.keys(values).filter(k => values[k] !== undefined && k in properties);
|
|
10256
|
+
return { ...shaped, required: [...new Set([...shaped['required'], ...given])] };
|
|
10257
|
+
}
|
|
10258
|
+
function sourceSchema(source) {
|
|
10259
|
+
if (source.kind === SitePlanSourceKind.Ai)
|
|
10260
|
+
return jsonShapedObject(SHAPE['sourceAi'], { kind: one$1(source.kind), runId: one$1(source.runId) }, 'sourceAi');
|
|
10261
|
+
return jsonShapedObject(SHAPE['sourceTemplate'], {
|
|
10262
|
+
kind: one$1(source.kind), templateId: one$1(source.templateId), templateVersion: { type: 'integer', enum: [source.templateVersion] },
|
|
10263
|
+
}, 'sourceTemplate');
|
|
10264
|
+
}
|
|
10265
|
+
function businessSchema() {
|
|
10266
|
+
const facts = jsonClosedObject(Object.fromEntries(FACT_KEYS.map(key => [key, jsonText()])), []);
|
|
10267
|
+
return jsonShapedObject(SHAPE['business'], { name: jsonText(), tagline: jsonText(), facts }, 'business');
|
|
10268
|
+
}
|
|
10269
|
+
/** A content type's field. */
|
|
10270
|
+
function fieldJsonSchema() {
|
|
10271
|
+
return jsonShapedObject(SHAPE['field'], { name: jsonText(), type: jsonText(), controller: jsonText(), required: jsonBoolean(), note: jsonText() }, 'field');
|
|
10272
|
+
}
|
|
10273
|
+
/** A content type, its seed's field values as a list (`values: [{ field, value }]`). */
|
|
10274
|
+
function contentTypeJsonSchema() {
|
|
10275
|
+
const value = jsonAnyOf([jsonText(), jsonListOf(jsonText())]);
|
|
10276
|
+
const values = jsonListOf(jsonClosedObject({ [SitePlanWireKey.SeedField]: jsonText(), [SitePlanWireKey.SeedValue]: value }, [SitePlanWireKey.SeedField, SitePlanWireKey.SeedValue]));
|
|
10277
|
+
const seed = jsonShapedObject(SHAPE['seed'], { count: jsonInteger(), sample: jsonBoolean(), isPublished: jsonBoolean() }, 'seed', { key: SitePlanWireKey.SeedValues, schema: values });
|
|
10278
|
+
return jsonShapedObject(SHAPE['contentType'], {
|
|
10279
|
+
name: jsonText(), label: jsonText(), previewTextField: jsonText(), previewImageField: jsonText(), fields: jsonListOf(fieldJsonSchema()), seed,
|
|
10280
|
+
}, 'contentType');
|
|
10281
|
+
}
|
|
10282
|
+
/** A data source: a cms list / single item, or a relative one. */
|
|
10283
|
+
function dataSourceJsonSchema() {
|
|
10284
|
+
const byRoute = jsonClosedObject({ field: jsonText(), routeParam: jsonText() }, ['field', 'routeParam']);
|
|
10285
|
+
const cms = jsonShapedObject(SHAPE['cmsDataSource'], {
|
|
10286
|
+
alias: jsonText(), type: one$1(PlanDataSourceType.Cms), mode: jsonOneOf(Object.values(PlanDataSourceMode)),
|
|
10287
|
+
contentType: jsonText(), limit: jsonInteger(), filterByRouteParam: byRoute,
|
|
10288
|
+
}, 'cmsDataSource');
|
|
10289
|
+
const relative = jsonShapedObject(SHAPE['relativeDataSource'], {
|
|
10290
|
+
alias: jsonText(), type: one$1(PlanDataSourceType.Relative), of: jsonText(), op: jsonOneOf(Object.values(RelativeOp)), within: jsonText(),
|
|
10291
|
+
}, 'relativeDataSource');
|
|
10292
|
+
return jsonAnyOf([cms, relative]);
|
|
10293
|
+
}
|
|
10294
|
+
/** The patterns a page section may use (every non-global one), in library order. */
|
|
10295
|
+
function sectionPatternIds() {
|
|
10296
|
+
return Object.entries(SITE_PATTERNS).filter(([, p]) => !p.global).map(([id]) => id);
|
|
10297
|
+
}
|
|
10298
|
+
/** A key a pattern has nothing for: only `{}` (or leaving it out) — the decoder drops it. */
|
|
10299
|
+
const NOTHING = () => jsonClosedObject({}, []);
|
|
10300
|
+
/** One section branch: this pattern's id, its declared slots (a list), its optional parts, a binding when it takes one. */
|
|
10301
|
+
function sectionBranch(id, pattern) {
|
|
10302
|
+
const slots = patternSlots(pattern);
|
|
10303
|
+
const parts = patternOptionalParts(pattern);
|
|
10304
|
+
const slotList = slots.length
|
|
10305
|
+
? jsonListOf(jsonClosedObject({ [SitePlanWireKey.SlotName]: jsonOneOf(slots), [SitePlanWireKey.SlotText]: jsonText() }, [SitePlanWireKey.SlotName, SitePlanWireKey.SlotText]))
|
|
10306
|
+
: NOTHING();
|
|
10307
|
+
const branch = jsonShapedObject(SHAPE['section'], {
|
|
10308
|
+
pattern: one$1(id),
|
|
10309
|
+
slots: slotList,
|
|
10310
|
+
binding: patternTakesBinding(pattern) ? jsonText() : NOTHING(),
|
|
10311
|
+
without: parts.length ? jsonListOf(jsonOneOf(parts)) : NOTHING(),
|
|
10312
|
+
}, 'section');
|
|
10313
|
+
return { ...branch, description: `${id}: ${pattern.label}` };
|
|
10314
|
+
}
|
|
10315
|
+
/** A page section: one branch per pattern. */
|
|
10316
|
+
function sectionJsonSchema() {
|
|
10317
|
+
return jsonAnyOf(sectionPatternIds().map(id => sectionBranch(id, SITE_PATTERNS[id])));
|
|
10318
|
+
}
|
|
10319
|
+
/** A page. */
|
|
10320
|
+
function pageJsonSchema() {
|
|
10321
|
+
return jsonShapedObject(SHAPE['page'], {
|
|
10322
|
+
key: jsonText(), name: jsonText(), route: jsonText(), isHome: jsonBoolean(), required: jsonBoolean(), inNav: jsonBoolean(),
|
|
10323
|
+
template: jsonText(), routeSource: jsonText(), sections: jsonListOf(sectionJsonSchema()),
|
|
10324
|
+
}, 'page');
|
|
10325
|
+
}
|
|
10326
|
+
const globalIds = (position) => Object.entries(SITE_PATTERNS).filter(([, p]) => p.global === position).map(([id]) => id);
|
|
10327
|
+
/** The Site Plan format, pinned to the owner's pick and this run. */
|
|
10328
|
+
function sitePlanJsonSchema(pin) {
|
|
10329
|
+
const globals = jsonShapedObject(SHAPE['globals'], {
|
|
10330
|
+
[GlobalSlot.Header]: jsonOneOf(globalIds(PatternGlobalPosition.Before)),
|
|
10331
|
+
[GlobalSlot.Footer]: jsonOneOf(globalIds(PatternGlobalPosition.After)),
|
|
10332
|
+
}, 'globals');
|
|
10333
|
+
return jsonShapedObject(SHAPE['plan'], {
|
|
10334
|
+
version: one$1(SITE_PLAN_VERSION),
|
|
10335
|
+
source: sourceSchema(pin.source),
|
|
10336
|
+
business: businessSchema(),
|
|
10337
|
+
design: pinnedDesign(pin.design),
|
|
10338
|
+
contentTypes: jsonListOf(contentTypeJsonSchema()),
|
|
10339
|
+
dataSources: jsonListOf(dataSourceJsonSchema()),
|
|
10340
|
+
pages: jsonListOf(pageJsonSchema()),
|
|
10341
|
+
globals,
|
|
10342
|
+
}, 'plan');
|
|
10343
|
+
}
|
|
10344
|
+
// ── The wire → the plan ─────────────────────────────────────────────────────────────────────────────
|
|
10345
|
+
const isEmptyObject = (v) => isPlainObject(v) && Object.keys(v).length === 0;
|
|
10346
|
+
/** `[{ slot, text }]` → `{ slot: text }`; anything that is not that list is left for the gate to refuse. */
|
|
10347
|
+
function slotsFromWire(slots) {
|
|
10348
|
+
if (!Array.isArray(slots))
|
|
10349
|
+
return slots;
|
|
10350
|
+
const out = {};
|
|
10351
|
+
for (const entry of slots) {
|
|
10352
|
+
if (!isPlainObject(entry) || typeof entry[SitePlanWireKey.SlotName] !== 'string')
|
|
10353
|
+
return slots;
|
|
10354
|
+
out[entry[SitePlanWireKey.SlotName]] = entry[SitePlanWireKey.SlotText];
|
|
10355
|
+
}
|
|
10356
|
+
return out;
|
|
10357
|
+
}
|
|
10358
|
+
/** A section off the wire: its slots back to a map; a key the pattern has nothing for (`{}`) dropped. */
|
|
10359
|
+
function sectionFromWire(section) {
|
|
10360
|
+
if (!isPlainObject(section))
|
|
10361
|
+
return section;
|
|
10362
|
+
const out = { ...section };
|
|
10363
|
+
for (const key of ['slots', 'binding', 'without'])
|
|
10364
|
+
if (isEmptyObject(out[key]))
|
|
10365
|
+
delete out[key];
|
|
10366
|
+
if ('slots' in out)
|
|
10367
|
+
out['slots'] = slotsFromWire(out['slots']);
|
|
10368
|
+
return out;
|
|
10369
|
+
}
|
|
10370
|
+
/** A content type off the wire: its seed's `values` list back to field keys. */
|
|
10371
|
+
function contentTypeFromWire(type) {
|
|
10372
|
+
if (!isPlainObject(type) || !isPlainObject(type['seed']))
|
|
10373
|
+
return type;
|
|
10374
|
+
const { [SitePlanWireKey.SeedValues]: values, ...seed } = type['seed'];
|
|
10375
|
+
if (Array.isArray(values)) {
|
|
10376
|
+
for (const entry of values) {
|
|
10377
|
+
if (isPlainObject(entry) && typeof entry[SitePlanWireKey.SeedField] === 'string')
|
|
10378
|
+
seed[entry[SitePlanWireKey.SeedField]] = entry[SitePlanWireKey.SeedValue];
|
|
10379
|
+
}
|
|
10380
|
+
}
|
|
10381
|
+
else if (values !== undefined)
|
|
10382
|
+
seed[SitePlanWireKey.SeedValues] = values;
|
|
10383
|
+
return { ...type, seed };
|
|
10384
|
+
}
|
|
10385
|
+
function pageFromWire(page) {
|
|
10386
|
+
if (!isPlainObject(page) || !Array.isArray(page['sections']))
|
|
10387
|
+
return page;
|
|
10388
|
+
return { ...page, sections: page['sections'].map(sectionFromWire) };
|
|
10389
|
+
}
|
|
10390
|
+
/** The design off the wire: an empty optional object (a choice the pick does not carry) dropped. */
|
|
10391
|
+
function designFromWire(design) {
|
|
10392
|
+
if (!isPlainObject(design))
|
|
10393
|
+
return design;
|
|
10394
|
+
return Object.fromEntries(Object.entries(design).filter(([, v]) => !isEmptyObject(v)));
|
|
10395
|
+
}
|
|
10396
|
+
/**
|
|
10397
|
+
* A model's answer → a Site Plan: the two listed maps back to maps, nothing else touched. Never repairs — whatever the
|
|
10398
|
+
* answer got wrong is still wrong, for `validateSitePlan` to refuse with a path.
|
|
10399
|
+
*/
|
|
10400
|
+
function sitePlanFromWire(wire) {
|
|
10401
|
+
if (!isPlainObject(wire))
|
|
10402
|
+
return wire;
|
|
10403
|
+
const out = { ...wire };
|
|
10404
|
+
if (Array.isArray(out['contentTypes']))
|
|
10405
|
+
out['contentTypes'] = out['contentTypes'].map(contentTypeFromWire);
|
|
10406
|
+
if (Array.isArray(out['pages']))
|
|
10407
|
+
out['pages'] = out['pages'].map(pageFromWire);
|
|
10408
|
+
if ('design' in out)
|
|
10409
|
+
out['design'] = designFromWire(out['design']);
|
|
10410
|
+
return out;
|
|
10411
|
+
}
|
|
10412
|
+
/** The plan → the wire (what a re-ask shows the model, and what the plan edits' prompt carries). */
|
|
10413
|
+
function sitePlanToWire(plan) {
|
|
10414
|
+
const slotsToWire = (slots) => Object.entries(slots ?? {}).map(([slot, text]) => ({ [SitePlanWireKey.SlotName]: slot, [SitePlanWireKey.SlotText]: text }));
|
|
10415
|
+
return {
|
|
10416
|
+
...plan,
|
|
10417
|
+
contentTypes: plan.contentTypes.map(t => {
|
|
10418
|
+
if (!t.seed)
|
|
10419
|
+
return t;
|
|
10420
|
+
const { count, sample, isPublished, ...values } = t.seed;
|
|
10421
|
+
return { ...t, seed: { count, sample, isPublished, [SitePlanWireKey.SeedValues]: Object.entries(values).map(([field, value]) => ({ [SitePlanWireKey.SeedField]: field, [SitePlanWireKey.SeedValue]: value })) } };
|
|
10422
|
+
}),
|
|
10423
|
+
pages: plan.pages.map(p => ({ ...p, sections: p.sections.map(s => (s.slots ? { ...s, slots: slotsToWire(s.slots) } : s)) })),
|
|
10424
|
+
};
|
|
10425
|
+
}
|
|
10426
|
+
|
|
10427
|
+
/**
|
|
10428
|
+
* Changing a plan by sentence (AI plan §3d "The Plan prompt box", M1-5; reused by M2 after a build): the CLOSED list
|
|
10429
|
+
* of operations (N-D6) a model may write against the current plan, and the ONE pure `applyPlanEdits` that carries them
|
|
10430
|
+
* out. Nothing is written until Build, so an edit is a plan → plan function; the result goes through
|
|
10431
|
+
* `validateSitePlan` like any plan. Pages are named by `key`, sections by their index on the page. A set of edits is
|
|
10432
|
+
* applied whole or not at all: one edit that cannot apply refuses the set, with a path, and the plan is unchanged.
|
|
10433
|
+
*/
|
|
10434
|
+
var PlanEditOp;
|
|
10435
|
+
(function (PlanEditOp) {
|
|
10436
|
+
PlanEditOp["AddPage"] = "addPage";
|
|
10437
|
+
PlanEditOp["RemovePage"] = "removePage";
|
|
10438
|
+
PlanEditOp["RenamePage"] = "renamePage";
|
|
10439
|
+
PlanEditOp["MergePages"] = "mergePages";
|
|
10440
|
+
/** Its place in the menu (the plan's page order). */
|
|
10441
|
+
PlanEditOp["MovePage"] = "movePage";
|
|
10442
|
+
PlanEditOp["AddSection"] = "addSection";
|
|
10443
|
+
PlanEditOp["RemoveSection"] = "removeSection";
|
|
10444
|
+
PlanEditOp["MoveSection"] = "moveSection";
|
|
10445
|
+
PlanEditOp["SwapPattern"] = "swapPattern";
|
|
10446
|
+
PlanEditOp["SetSlot"] = "setSlot";
|
|
10447
|
+
PlanEditOp["AddContentType"] = "addContentType";
|
|
10448
|
+
PlanEditOp["AddField"] = "addField";
|
|
10449
|
+
PlanEditOp["SetSectionData"] = "setSectionData";
|
|
10450
|
+
})(PlanEditOp || (PlanEditOp = {}));
|
|
10451
|
+
/**
|
|
10452
|
+
* Every op's keys, explained — what the format is built from and what `validatePlanEdits` reads (`key?` optional).
|
|
10453
|
+
* A page is named by its `key`; a section by its 0-based `index` on that page.
|
|
10454
|
+
*/
|
|
10455
|
+
const PLAN_EDIT_SHAPE = {
|
|
10456
|
+
[PlanEditOp.AddPage]: { op: "'addPage'", page: 'page — a whole new page (a unique key and route)', 'after?': 'string — the key of the page it follows in the menu; absent: last' },
|
|
10457
|
+
[PlanEditOp.RemovePage]: { op: "'removePage'", page: 'string — the key of the page to remove (never the home page)' },
|
|
10458
|
+
[PlanEditOp.RenamePage]: { op: "'renamePage'", page: 'string — a page key', 'name?': 'string — its new name (the menu label)', 'route?': 'string — its new route' },
|
|
10459
|
+
[PlanEditOp.MergePages]: { op: "'mergePages'", into: 'string — the page that stays', from: 'string — the page whose sections move into it, then removed' },
|
|
10460
|
+
[PlanEditOp.MovePage]: { op: "'movePage'", page: 'string — a page key', to: 'integer — its new 0-based place in the menu' },
|
|
10461
|
+
[PlanEditOp.AddSection]: { op: "'addSection'", page: 'string — a page key', section: 'section', 'at?': 'integer — its 0-based place on the page; absent: last' },
|
|
10462
|
+
[PlanEditOp.RemoveSection]: { op: "'removeSection'", page: 'string — a page key', index: 'integer — the section' },
|
|
10463
|
+
[PlanEditOp.MoveSection]: { op: "'moveSection'", page: 'string — a page key', from: 'integer — the section', to: 'integer — its new place' },
|
|
10464
|
+
[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)' },
|
|
10465
|
+
[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" },
|
|
10466
|
+
[PlanEditOp.AddContentType]: { op: "'addContentType'", contentType: 'contentType — a unique name', 'dataSources?': 'dataSource[] — the list / single sources its pages read' },
|
|
10467
|
+
[PlanEditOp.AddField]: { op: "'addField'", contentType: 'string — a content type of the plan', field: 'field', 'sample?': "string | string[] — the samples' value for it" },
|
|
10468
|
+
[PlanEditOp.SetSectionData]: { op: "'setSectionData'", page: 'string — a page key', index: 'integer — the section', binding: 'string — the list source the section repeats' },
|
|
10469
|
+
};
|
|
10470
|
+
var PlanChangeKind;
|
|
10471
|
+
(function (PlanChangeKind) {
|
|
10472
|
+
PlanChangeKind["Page"] = "page";
|
|
10473
|
+
PlanChangeKind["Section"] = "section";
|
|
10474
|
+
PlanChangeKind["ContentType"] = "contentType";
|
|
10475
|
+
PlanChangeKind["Field"] = "field";
|
|
10476
|
+
PlanChangeKind["DataSource"] = "dataSource";
|
|
10477
|
+
})(PlanChangeKind || (PlanChangeKind = {}));
|
|
10478
|
+
var PlanChangeAction;
|
|
10479
|
+
(function (PlanChangeAction) {
|
|
10480
|
+
PlanChangeAction["Added"] = "added";
|
|
10481
|
+
PlanChangeAction["Removed"] = "removed";
|
|
10482
|
+
PlanChangeAction["Changed"] = "changed";
|
|
10483
|
+
PlanChangeAction["Moved"] = "moved";
|
|
10484
|
+
})(PlanChangeAction || (PlanChangeAction = {}));
|
|
10485
|
+
var PlanEditIssueCode;
|
|
10486
|
+
(function (PlanEditIssueCode) {
|
|
10487
|
+
PlanEditIssueCode["Shape"] = "shape";
|
|
10488
|
+
PlanEditIssueCode["UnknownKey"] = "unknown-key";
|
|
10489
|
+
PlanEditIssueCode["Duplicate"] = "duplicate";
|
|
10490
|
+
PlanEditIssueCode["UnknownOp"] = "unknown-op";
|
|
10491
|
+
PlanEditIssueCode["UnknownPage"] = "unknown-page";
|
|
10492
|
+
PlanEditIssueCode["UnknownSection"] = "unknown-section";
|
|
10493
|
+
PlanEditIssueCode["UnknownPattern"] = "unknown-pattern";
|
|
10494
|
+
PlanEditIssueCode["UnknownSlot"] = "unknown-slot";
|
|
10495
|
+
PlanEditIssueCode["UnknownContentType"] = "unknown-content-type";
|
|
10496
|
+
PlanEditIssueCode["UnknownDataSource"] = "unknown-data-source";
|
|
10497
|
+
/** The home page stays (a plan has exactly one). */
|
|
10498
|
+
PlanEditIssueCode["HomePage"] = "home-page";
|
|
10499
|
+
/** A template page (one item per route) and a plain page cannot be merged. */
|
|
10500
|
+
PlanEditIssueCode["MergeTemplate"] = "merge-template";
|
|
10501
|
+
})(PlanEditIssueCode || (PlanEditIssueCode = {}));
|
|
10502
|
+
class Collector extends PathIssueCollector {
|
|
10503
|
+
constructor() {
|
|
10504
|
+
super('plan edit', { shape: PlanEditIssueCode.Shape, unknownKey: PlanEditIssueCode.UnknownKey, duplicate: PlanEditIssueCode.Duplicate });
|
|
10505
|
+
}
|
|
10506
|
+
}
|
|
10507
|
+
const OPS = Object.values(PlanEditOp);
|
|
10508
|
+
/** A page's own header section (`page-header.simple`) — what a merge leaves behind with the page it named. */
|
|
10509
|
+
const PAGE_HEADER_FAMILY = 'header';
|
|
10510
|
+
const clone = (v) => JSON.parse(JSON.stringify(v));
|
|
10511
|
+
const clamp = (n, max) => Math.max(0, Math.min(Math.trunc(n), max));
|
|
10512
|
+
/** The edits' structure: a known op, its keys (`PLAN_EDIT_SHAPE`), integers where an index is. Never repairs. */
|
|
10513
|
+
function validatePlanEdits(edits) {
|
|
10514
|
+
const c = new Collector();
|
|
10515
|
+
if (!Array.isArray(edits)) {
|
|
10516
|
+
c.add(PlanEditIssueCode.Shape, 'edits', 'must be an array');
|
|
10517
|
+
return { valid: false, issues: c.issues };
|
|
10518
|
+
}
|
|
10519
|
+
edits.forEach((raw, i) => {
|
|
10520
|
+
const path = `edits[${i}]`;
|
|
10521
|
+
if (!isPlainObject(raw))
|
|
10522
|
+
return c.add(PlanEditIssueCode.Shape, path, 'must be an object');
|
|
10523
|
+
const op = raw['op'];
|
|
10524
|
+
if (typeof op !== 'string' || !OPS.includes(op))
|
|
10525
|
+
return c.add(PlanEditIssueCode.UnknownOp, `${path}.op`, `must be one of ${OPS.join(', ')}`);
|
|
10526
|
+
const entry = PLAN_EDIT_SHAPE[op];
|
|
10527
|
+
c.keys(raw, shapeKeys(entry), path);
|
|
10528
|
+
for (const [key, text] of Object.entries(entry)) {
|
|
10529
|
+
const optional = key.endsWith('?');
|
|
10530
|
+
const name = optional ? key.slice(0, -1) : key;
|
|
10531
|
+
const v = raw[name];
|
|
10532
|
+
if (v === undefined) {
|
|
10533
|
+
if (!optional)
|
|
10534
|
+
c.add(PlanEditIssueCode.Shape, `${path}.${name}`, 'is required');
|
|
10535
|
+
continue;
|
|
10536
|
+
}
|
|
10537
|
+
if (text.startsWith('string | string[]')) {
|
|
10538
|
+
if (typeof v !== 'string' && !(Array.isArray(v) && v.every(x => typeof x === 'string')))
|
|
10539
|
+
c.add(PlanEditIssueCode.Shape, `${path}.${name}`, 'must be a string or a list of strings');
|
|
10540
|
+
}
|
|
10541
|
+
else if (text.startsWith('string') && !isNonEmptyString(v))
|
|
10542
|
+
c.add(PlanEditIssueCode.Shape, `${path}.${name}`, 'must be a non-empty string');
|
|
10543
|
+
if (text.startsWith('integer') && !Number.isInteger(v))
|
|
10544
|
+
c.add(PlanEditIssueCode.Shape, `${path}.${name}`, 'must be a whole number');
|
|
10545
|
+
if (['page', 'section', 'contentType', 'field'].includes(text.split(' ')[0]) && !isPlainObject(v))
|
|
10546
|
+
c.add(PlanEditIssueCode.Shape, `${path}.${name}`, 'must be an object');
|
|
10547
|
+
if (text.startsWith('dataSource[]') && !Array.isArray(v))
|
|
10548
|
+
c.add(PlanEditIssueCode.Shape, `${path}.${name}`, 'must be an array');
|
|
10549
|
+
}
|
|
10550
|
+
});
|
|
10551
|
+
return { valid: c.issues.length === 0, issues: c.issues };
|
|
10552
|
+
}
|
|
10553
|
+
/** The aliases a page reads (its route source and its sections' bindings). */
|
|
10554
|
+
function aliasesOf(pages) {
|
|
10555
|
+
const out = new Set();
|
|
10556
|
+
for (const page of pages) {
|
|
10557
|
+
if (page.routeSource)
|
|
10558
|
+
out.add(page.routeSource);
|
|
10559
|
+
for (const s of page.sections)
|
|
10560
|
+
if (s.binding)
|
|
10561
|
+
out.add(s.binding);
|
|
10562
|
+
}
|
|
10563
|
+
return out;
|
|
10564
|
+
}
|
|
10565
|
+
/** A relative source (`project.next`'s) is read while its subject is, and keeps the list it walks. */
|
|
10566
|
+
function withDependencies(aliases, sources) {
|
|
10567
|
+
const out = new Set(aliases);
|
|
10568
|
+
for (const s of sources) {
|
|
10569
|
+
if (s.type === PlanDataSourceType.Relative && aliases.has(s.of)) {
|
|
10570
|
+
out.add(s.alias);
|
|
10571
|
+
out.add(s.within);
|
|
10572
|
+
}
|
|
10573
|
+
}
|
|
10574
|
+
return out;
|
|
10575
|
+
}
|
|
10576
|
+
const pageIndex = (d, key) => d.pages.findIndex(p => p.key === key);
|
|
10577
|
+
function findPage(d, key, c, path) {
|
|
10578
|
+
const at = pageIndex(d, key);
|
|
10579
|
+
if (at < 0)
|
|
10580
|
+
c.add(PlanEditIssueCode.UnknownPage, path, `no page "${String(key)}" in the plan (${d.pages.map(p => p.key).join(', ')})`);
|
|
10581
|
+
return at;
|
|
10582
|
+
}
|
|
10583
|
+
function findSection(d, pageAt, index, c, path) {
|
|
10584
|
+
const ok = Number.isInteger(index) && index >= 0 && index < d.pages[pageAt].sections.length;
|
|
10585
|
+
if (!ok)
|
|
10586
|
+
c.add(PlanEditIssueCode.UnknownSection, path, `page "${d.pages[pageAt].key}" has sections 0–${d.pages[pageAt].sections.length - 1}`);
|
|
10587
|
+
return ok;
|
|
10588
|
+
}
|
|
10589
|
+
const setSections = (d, at, sections) => { d.pages[at] = { ...d.pages[at], sections }; };
|
|
10590
|
+
/** One handler per op — a data table, never an if-chain. */
|
|
10591
|
+
const APPLY = {
|
|
10592
|
+
[PlanEditOp.AddPage]: (d, e, c, path, changed) => {
|
|
10593
|
+
const page = { ...e.page, sections: (e.page.sections ?? []).map(sectionFromWire) };
|
|
10594
|
+
if (pageIndex(d, page.key) >= 0)
|
|
10595
|
+
return c.add(PlanEditIssueCode.Duplicate, `${path}.page.key`, `page "${page.key}" is already in the plan`);
|
|
10596
|
+
const after = e.after === undefined ? d.pages.length - 1 : findPage(d, e.after, c, `${path}.after`);
|
|
10597
|
+
if (after < 0 && e.after !== undefined)
|
|
10598
|
+
return;
|
|
10599
|
+
d.pages.splice(after + 1, 0, page);
|
|
10600
|
+
changed.push({ kind: PlanChangeKind.Page, action: PlanChangeAction.Added, page: page.key, name: page.name });
|
|
10601
|
+
},
|
|
10602
|
+
[PlanEditOp.RemovePage]: (d, e, c, path, changed) => {
|
|
10603
|
+
const at = findPage(d, e.page, c, `${path}.page`);
|
|
10604
|
+
if (at < 0)
|
|
10605
|
+
return;
|
|
10606
|
+
if (d.pages[at].isHome)
|
|
10607
|
+
return c.add(PlanEditIssueCode.HomePage, `${path}.page`, 'the home page stays — rename or change it instead');
|
|
10608
|
+
const [gone] = d.pages.splice(at, 1);
|
|
10609
|
+
changed.push({ kind: PlanChangeKind.Page, action: PlanChangeAction.Removed, page: gone.key, name: gone.name });
|
|
10610
|
+
},
|
|
10611
|
+
[PlanEditOp.RenamePage]: (d, e, c, path, changed) => {
|
|
10612
|
+
const at = findPage(d, e.page, c, `${path}.page`);
|
|
10613
|
+
if (at < 0)
|
|
10614
|
+
return;
|
|
10615
|
+
d.pages[at] = { ...d.pages[at], ...(e.name !== undefined ? { name: e.name } : {}), ...(e.route !== undefined ? { route: e.route } : {}) };
|
|
10616
|
+
changed.push({ kind: PlanChangeKind.Page, action: PlanChangeAction.Changed, page: d.pages[at].key, name: d.pages[at].name });
|
|
10617
|
+
},
|
|
10618
|
+
[PlanEditOp.MergePages]: (d, e, c, path, changed) => {
|
|
10619
|
+
const into = findPage(d, e.into, c, `${path}.into`);
|
|
10620
|
+
const from = findPage(d, e.from, c, `${path}.from`);
|
|
10621
|
+
if (into < 0 || from < 0)
|
|
10622
|
+
return;
|
|
10623
|
+
if (into === from)
|
|
10624
|
+
return c.add(PlanEditIssueCode.Duplicate, `${path}.from`, 'a page is merged into another page');
|
|
10625
|
+
const a = d.pages[into];
|
|
10626
|
+
const b = d.pages[from];
|
|
10627
|
+
if (!!a.template !== !!b.template)
|
|
10628
|
+
return c.add(PlanEditIssueCode.MergeTemplate, `${path}.from`, 'a page per item and a plain page cannot be merged');
|
|
10629
|
+
if (b.isHome)
|
|
10630
|
+
return c.add(PlanEditIssueCode.HomePage, `${path}.from`, 'the home page stays — merge the other page into it');
|
|
10631
|
+
const moved = b.sections.filter((s, i) => !(i === 0 && patternById(s.pattern)?.family === PAGE_HEADER_FAMILY));
|
|
10632
|
+
setSections(d, into, [...a.sections, ...moved]);
|
|
10633
|
+
moved.forEach((_, k) => changed.push({ kind: PlanChangeKind.Section, action: PlanChangeAction.Added, page: a.key, index: a.sections.length + k }));
|
|
10634
|
+
d.pages.splice(from, 1);
|
|
10635
|
+
changed.push({ kind: PlanChangeKind.Page, action: PlanChangeAction.Changed, page: a.key, name: a.name });
|
|
10636
|
+
changed.push({ kind: PlanChangeKind.Page, action: PlanChangeAction.Removed, page: b.key, name: b.name });
|
|
10637
|
+
},
|
|
10638
|
+
[PlanEditOp.MovePage]: (d, e, c, path, changed) => {
|
|
10639
|
+
const at = findPage(d, e.page, c, `${path}.page`);
|
|
10640
|
+
if (at < 0)
|
|
10641
|
+
return;
|
|
10642
|
+
const [page] = d.pages.splice(at, 1);
|
|
10643
|
+
d.pages.splice(clamp(e.to, d.pages.length), 0, page);
|
|
10644
|
+
changed.push({ kind: PlanChangeKind.Page, action: PlanChangeAction.Moved, page: page.key, name: page.name });
|
|
10645
|
+
},
|
|
10646
|
+
[PlanEditOp.AddSection]: (d, e, c, path, changed) => {
|
|
10647
|
+
const at = findPage(d, e.page, c, `${path}.page`);
|
|
10648
|
+
if (at < 0)
|
|
10649
|
+
return;
|
|
10650
|
+
const sections = [...d.pages[at].sections];
|
|
10651
|
+
const index = e.at === undefined ? sections.length : clamp(e.at, sections.length);
|
|
10652
|
+
sections.splice(index, 0, sectionFromWire(e.section));
|
|
10653
|
+
setSections(d, at, sections);
|
|
10654
|
+
changed.push({ kind: PlanChangeKind.Section, action: PlanChangeAction.Added, page: d.pages[at].key, index });
|
|
10655
|
+
},
|
|
10656
|
+
[PlanEditOp.RemoveSection]: (d, e, c, path, changed) => {
|
|
10657
|
+
const at = findPage(d, e.page, c, `${path}.page`);
|
|
10658
|
+
if (at < 0 || !findSection(d, at, e.index, c, `${path}.index`))
|
|
10659
|
+
return;
|
|
10660
|
+
setSections(d, at, d.pages[at].sections.filter((_, i) => i !== e.index));
|
|
10661
|
+
changed.push({ kind: PlanChangeKind.Section, action: PlanChangeAction.Removed, page: d.pages[at].key, index: e.index });
|
|
10662
|
+
},
|
|
10663
|
+
[PlanEditOp.MoveSection]: (d, e, c, path, changed) => {
|
|
10664
|
+
const at = findPage(d, e.page, c, `${path}.page`);
|
|
10665
|
+
if (at < 0 || !findSection(d, at, e.from, c, `${path}.from`))
|
|
10666
|
+
return;
|
|
10667
|
+
const sections = [...d.pages[at].sections];
|
|
10668
|
+
const [section] = sections.splice(e.from, 1);
|
|
10669
|
+
const to = clamp(e.to, sections.length);
|
|
10670
|
+
sections.splice(to, 0, section);
|
|
10671
|
+
setSections(d, at, sections);
|
|
10672
|
+
changed.push({ kind: PlanChangeKind.Section, action: PlanChangeAction.Moved, page: d.pages[at].key, index: to });
|
|
10673
|
+
},
|
|
10674
|
+
[PlanEditOp.SwapPattern]: (d, e, c, path, changed) => {
|
|
10675
|
+
const at = findPage(d, e.page, c, `${path}.page`);
|
|
10676
|
+
if (at < 0 || !findSection(d, at, e.index, c, `${path}.index`))
|
|
10677
|
+
return;
|
|
10678
|
+
const next = patternById(e.pattern);
|
|
10679
|
+
if (!next || next.global)
|
|
10680
|
+
return c.add(PlanEditIssueCode.UnknownPattern, `${path}.pattern`, `no section pattern "${String(e.pattern)}"`);
|
|
10681
|
+
const old = d.pages[at].sections[e.index];
|
|
10682
|
+
const slots = Object.fromEntries(Object.entries(old.slots ?? {}).filter(([slot]) => patternSlots(next).includes(slot)));
|
|
10683
|
+
const without = (old.without ?? []).filter(w => patternOptionalParts(next).includes(w));
|
|
10684
|
+
const section = {
|
|
10685
|
+
pattern: e.pattern,
|
|
10686
|
+
...(Object.keys(slots).length ? { slots } : {}),
|
|
10687
|
+
...(old.binding && patternTakesBinding(next) ? { binding: old.binding } : {}),
|
|
10688
|
+
...(without.length ? { without } : {}),
|
|
10689
|
+
};
|
|
10690
|
+
setSections(d, at, d.pages[at].sections.map((s, i) => (i === e.index ? section : s)));
|
|
10691
|
+
changed.push({ kind: PlanChangeKind.Section, action: PlanChangeAction.Changed, page: d.pages[at].key, index: e.index });
|
|
10692
|
+
},
|
|
10693
|
+
[PlanEditOp.SetSlot]: (d, e, c, path, changed) => {
|
|
10694
|
+
const at = findPage(d, e.page, c, `${path}.page`);
|
|
10695
|
+
if (at < 0 || !findSection(d, at, e.index, c, `${path}.index`))
|
|
10696
|
+
return;
|
|
10697
|
+
const section = d.pages[at].sections[e.index];
|
|
10698
|
+
const declared = patternSlots(patternById(section.pattern));
|
|
10699
|
+
if (!declared.includes(e.slot))
|
|
10700
|
+
return c.add(PlanEditIssueCode.UnknownSlot, `${path}.slot`, `"${section.pattern}" has no slot "${e.slot}" (${declared.join(', ') || 'none'})`);
|
|
10701
|
+
setSections(d, at, d.pages[at].sections.map((s, i) => (i === e.index ? { ...s, slots: { ...(s.slots ?? {}), [e.slot]: e.text } } : s)));
|
|
10702
|
+
changed.push({ kind: PlanChangeKind.Section, action: PlanChangeAction.Changed, page: d.pages[at].key, index: e.index });
|
|
10703
|
+
},
|
|
10704
|
+
[PlanEditOp.AddContentType]: (d, e, c, path, changed) => {
|
|
10705
|
+
const type = contentTypeFromWire(e.contentType);
|
|
10706
|
+
if (d.contentTypes.some(t => t.name === type.name))
|
|
10707
|
+
return c.add(PlanEditIssueCode.Duplicate, `${path}.contentType.name`, `content type "${type.name}" is already in the plan`);
|
|
10708
|
+
d.contentTypes.push(type);
|
|
10709
|
+
changed.push({ kind: PlanChangeKind.ContentType, action: PlanChangeAction.Added, name: type.name });
|
|
10710
|
+
for (const [k, source] of (e.dataSources ?? []).entries()) {
|
|
10711
|
+
if (d.dataSources.some(s => s.alias === source.alias)) {
|
|
10712
|
+
c.add(PlanEditIssueCode.Duplicate, `${path}.dataSources[${k}].alias`, `data source "${source.alias}" is already in the plan`);
|
|
10713
|
+
continue;
|
|
10714
|
+
}
|
|
10715
|
+
d.dataSources.push(source);
|
|
10716
|
+
changed.push({ kind: PlanChangeKind.DataSource, action: PlanChangeAction.Added, name: source.alias });
|
|
10717
|
+
}
|
|
10718
|
+
},
|
|
10719
|
+
[PlanEditOp.AddField]: (d, e, c, path, changed) => {
|
|
10720
|
+
const at = d.contentTypes.findIndex(t => t.name === e.contentType);
|
|
10721
|
+
if (at < 0)
|
|
10722
|
+
return c.add(PlanEditIssueCode.UnknownContentType, `${path}.contentType`, `no content type "${String(e.contentType)}" in the plan`);
|
|
10723
|
+
const type = d.contentTypes[at];
|
|
10724
|
+
if (type.fields.some(f => f.name === e.field.name))
|
|
10725
|
+
return c.add(PlanEditIssueCode.Duplicate, `${path}.field.name`, `"${type.name}" already has a field "${e.field.name}"`);
|
|
10726
|
+
const seed = type.seed ? { ...type.seed, [e.field.name]: e.sample ?? '' } : undefined;
|
|
10727
|
+
d.contentTypes[at] = { ...type, fields: [...type.fields, e.field], ...(seed ? { seed } : {}) };
|
|
10728
|
+
changed.push({ kind: PlanChangeKind.Field, action: PlanChangeAction.Added, name: `${type.name}.${e.field.name}` });
|
|
10729
|
+
},
|
|
10730
|
+
[PlanEditOp.SetSectionData]: (d, e, c, path, changed) => {
|
|
10731
|
+
const at = findPage(d, e.page, c, `${path}.page`);
|
|
10732
|
+
if (at < 0 || !findSection(d, at, e.index, c, `${path}.index`))
|
|
10733
|
+
return;
|
|
10734
|
+
const source = d.dataSources.find(s => s.alias === e.binding);
|
|
10735
|
+
if (!source || source.type !== PlanDataSourceType.Cms || source.mode !== PlanDataSourceMode.Query) {
|
|
10736
|
+
return c.add(PlanEditIssueCode.UnknownDataSource, `${path}.binding`, `no list source "${String(e.binding)}" in the plan`);
|
|
10737
|
+
}
|
|
10738
|
+
setSections(d, at, d.pages[at].sections.map((s, i) => (i === e.index ? { ...s, binding: e.binding } : s)));
|
|
10739
|
+
changed.push({ kind: PlanChangeKind.Section, action: PlanChangeAction.Changed, page: d.pages[at].key, index: e.index });
|
|
10740
|
+
},
|
|
10741
|
+
};
|
|
10742
|
+
/**
|
|
10743
|
+
* Apply a set of edits to a plan — all of them, or none (any issue refuses the set and returns the plan unchanged). The
|
|
10744
|
+
* sources only removed pages and sections read are dropped with them; sources nothing read before are kept.
|
|
10745
|
+
* The result still has to pass `validateSitePlan`.
|
|
10746
|
+
*/
|
|
10747
|
+
function applyPlanEdits(plan, edits) {
|
|
10748
|
+
const structure = validatePlanEdits(edits);
|
|
10749
|
+
if (!structure.valid)
|
|
10750
|
+
return { plan, changed: [], issues: structure.issues };
|
|
10751
|
+
const draft = { pages: clone([...plan.pages]), contentTypes: clone([...plan.contentTypes]), dataSources: clone([...plan.dataSources]) };
|
|
10752
|
+
const c = new Collector();
|
|
10753
|
+
const changed = [];
|
|
10754
|
+
edits.forEach((edit, i) => APPLY[edit.op](draft, edit, c, `edits[${i}]`, changed));
|
|
10755
|
+
if (c.issues.length)
|
|
10756
|
+
return { plan, changed: [], issues: c.issues };
|
|
10757
|
+
const before = withDependencies(aliasesOf(plan.pages), plan.dataSources);
|
|
10758
|
+
const after = withDependencies(aliasesOf(draft.pages), draft.dataSources);
|
|
10759
|
+
const added = new Set(changed.filter(ch => ch.kind === PlanChangeKind.DataSource).map(ch => ch.name));
|
|
10760
|
+
const dataSources = draft.dataSources.filter(s => after.has(s.alias) || !before.has(s.alias) || added.has(s.alias));
|
|
10761
|
+
for (const s of draft.dataSources)
|
|
10762
|
+
if (!dataSources.includes(s))
|
|
10763
|
+
changed.push({ kind: PlanChangeKind.DataSource, action: PlanChangeAction.Removed, name: s.alias });
|
|
10764
|
+
return { plan: { ...plan, pages: draft.pages, contentTypes: draft.contentTypes, dataSources }, changed, issues: [] };
|
|
10765
|
+
}
|
|
10766
|
+
// ── The format a model writes edits in ─────────────────────────────────────────────────────────────────
|
|
10767
|
+
const PLAN_EDITS_SCHEMA_NAME = 'plan_edits';
|
|
10768
|
+
/** A sentence's answer: the edits, or why the ops cannot say it (one line, in the owner's language). */
|
|
10769
|
+
var PlanEditsOutcome;
|
|
10770
|
+
(function (PlanEditsOutcome) {
|
|
10771
|
+
PlanEditsOutcome["Edits"] = "edits";
|
|
10772
|
+
PlanEditsOutcome["Cannot"] = "cannot";
|
|
10773
|
+
})(PlanEditsOutcome || (PlanEditsOutcome = {}));
|
|
10774
|
+
/** The answer's own keys, explained. */
|
|
10775
|
+
const PLAN_EDITS_ANSWER_SHAPE = {
|
|
10776
|
+
outcome: "'edits' | 'cannot' — cannot: the operations cannot do what the sentence asks",
|
|
10777
|
+
edits: 'edit[] — applied in order, all or none; empty when cannot',
|
|
10778
|
+
reason: "string — when cannot, ONE short line for the owner in their language (what to do instead); else ''",
|
|
10779
|
+
};
|
|
10780
|
+
const one = (value) => jsonOneOf([value]);
|
|
10781
|
+
/** The schema of each op's value types (what `PLAN_EDIT_SHAPE` names). */
|
|
10782
|
+
function opSchema(op) {
|
|
10783
|
+
const entry = PLAN_EDIT_SHAPE[op];
|
|
10784
|
+
const valueOf = (key) => {
|
|
10785
|
+
const text = entry[key] ?? entry[`${key}?`];
|
|
10786
|
+
if (key === 'op')
|
|
10787
|
+
return one(op);
|
|
10788
|
+
if (key === 'pattern')
|
|
10789
|
+
return jsonOneOf(sectionPatternIds());
|
|
10790
|
+
const head = text.split(' ')[0];
|
|
10791
|
+
const table = {
|
|
10792
|
+
page: pageJsonSchema, section: sectionJsonSchema, contentType: contentTypeJsonSchema, field: fieldJsonSchema,
|
|
10793
|
+
'dataSource[]': () => jsonListOf(dataSourceJsonSchema()), integer: jsonInteger, string: jsonText,
|
|
10794
|
+
};
|
|
10795
|
+
if (text.startsWith('string | string[]'))
|
|
10796
|
+
return jsonAnyOf([jsonText(), jsonListOf(jsonText())]);
|
|
10797
|
+
return (table[head] ?? jsonText)();
|
|
10798
|
+
};
|
|
10799
|
+
return jsonShapedObject(entry, Object.fromEntries(shapeKeys(entry).map(key => [key, valueOf(key)])), op);
|
|
10800
|
+
}
|
|
10801
|
+
/** The format a sentence's answer is written in: the outcome, the edits (one `anyOf` branch per op), the reason. */
|
|
10802
|
+
function planEditsJsonSchema() {
|
|
10803
|
+
return jsonShapedObject(PLAN_EDITS_ANSWER_SHAPE, {
|
|
10804
|
+
outcome: jsonOneOf(Object.values(PlanEditsOutcome)),
|
|
10805
|
+
edits: jsonListOf(jsonAnyOf(OPS.map(op => opSchema(op)))),
|
|
10806
|
+
reason: jsonText(),
|
|
10807
|
+
}, 'planEdits');
|
|
10808
|
+
}
|
|
10809
|
+
/** A model's answer → edits (sections and content types off the wire); never repaired. */
|
|
10810
|
+
function planEditsFromWire(answer) {
|
|
10811
|
+
if (!isPlainObject(answer) || !Array.isArray(answer['edits']))
|
|
10812
|
+
return answer;
|
|
10813
|
+
const edits = answer['edits'].map(raw => {
|
|
10814
|
+
if (!isPlainObject(raw))
|
|
10815
|
+
return raw;
|
|
10816
|
+
const e = { ...raw };
|
|
10817
|
+
if (isPlainObject(e['page']) && Array.isArray(e['page']['sections'])) {
|
|
10818
|
+
e['page'] = { ...e['page'], sections: e['page']['sections'].map(sectionFromWire) };
|
|
10819
|
+
}
|
|
10820
|
+
if (isPlainObject(e['section']))
|
|
10821
|
+
e['section'] = sectionFromWire(e['section']);
|
|
10822
|
+
if (isPlainObject(e['contentType']))
|
|
10823
|
+
e['contentType'] = contentTypeFromWire(e['contentType']);
|
|
10824
|
+
return e;
|
|
10825
|
+
});
|
|
10826
|
+
return { ...answer, edits };
|
|
10827
|
+
}
|
|
10828
|
+
|
|
10829
|
+
/**
|
|
10830
|
+
* The plan stage's word checks (AI plan §3d / §3h, M1-3): every text a model WROTE into a plan — the tagline, page
|
|
10831
|
+
* names, section slots, content-type labels, sample values — scanned for the words the owner and their kind of
|
|
10832
|
+
* business must not use (`avoidWords`, the analysis's + the type's `words`), and for a fact the owner never gave
|
|
10833
|
+
* (an email, phone number, price or opening time that is not one of the owner's own facts). A finding is a refusal
|
|
10834
|
+
* with a path — the model rewrites; nothing here repairs a word. Pure.
|
|
10835
|
+
*/
|
|
10836
|
+
var PlanWordIssueCode;
|
|
10837
|
+
(function (PlanWordIssueCode) {
|
|
10838
|
+
/** A word the owner (or their kind of business) must not use. */
|
|
10839
|
+
PlanWordIssueCode["AvoidWord"] = "avoid-word";
|
|
10840
|
+
/** Something that looks like a fact — an email, phone, price or time — that is not one of the owner's facts. */
|
|
10841
|
+
PlanWordIssueCode["InventedFact"] = "invented-fact";
|
|
10842
|
+
/** `business.facts` holds a fact the owner did not give, or not in the owner's words. */
|
|
10843
|
+
PlanWordIssueCode["FactNotTheOwners"] = "fact-not-the-owners";
|
|
10844
|
+
})(PlanWordIssueCode || (PlanWordIssueCode = {}));
|
|
10845
|
+
/** Every text in a plan a model writes (never ids, routes, field names or pattern ids). */
|
|
10846
|
+
function writtenTexts(plan) {
|
|
10847
|
+
const out = [];
|
|
10848
|
+
const add = (path, text) => { if (typeof text === 'string' && text.trim())
|
|
10849
|
+
out.push({ path, text }); };
|
|
10850
|
+
add('business.tagline', plan.business?.tagline);
|
|
10851
|
+
plan.pages?.forEach((page, p) => {
|
|
10852
|
+
add(`pages[${p}].name`, page.name);
|
|
10853
|
+
page.sections?.forEach((section, s) => {
|
|
10854
|
+
for (const [slot, text] of Object.entries(section.slots ?? {}))
|
|
10855
|
+
add(`pages[${p}].sections[${s}].slots.${slot}`, text);
|
|
10856
|
+
});
|
|
10857
|
+
});
|
|
10858
|
+
plan.contentTypes?.forEach((type, t) => {
|
|
10859
|
+
add(`contentTypes[${t}].label`, type.label);
|
|
10860
|
+
for (const [field, value] of Object.entries(type.seed ?? {})) {
|
|
10861
|
+
const values = Array.isArray(value) ? value : [value];
|
|
10862
|
+
values.forEach((v, k) => add(`contentTypes[${t}].seed.${field}${Array.isArray(value) ? `[${k}]` : ''}`, v));
|
|
10863
|
+
}
|
|
10864
|
+
});
|
|
10865
|
+
return out;
|
|
10866
|
+
}
|
|
10867
|
+
const HEBREW = /[-]/;
|
|
10868
|
+
/** Hebrew joins up to two one-letter words (and, the, in, to, from, that, as) to the next: "והטוב" holds "טוב". */
|
|
10869
|
+
const HEBREW_PREFIXES = '[והבלמשכ]{0,2}';
|
|
10870
|
+
const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
10871
|
+
const normalise = (s) => s.normalize('NFKC').toLowerCase();
|
|
10872
|
+
/** A word (or phrase) as a whole-word pattern; Hebrew allows its one-letter prefixes. */
|
|
10873
|
+
function wordPattern(word) {
|
|
10874
|
+
const w = escape(normalise(word.trim())).replace(/\s+/g, '\\s+');
|
|
10875
|
+
const prefix = HEBREW.test(word) ? HEBREW_PREFIXES : '';
|
|
10876
|
+
return new RegExp(`(?<![\\p{L}\\p{N}])${prefix}${w}(?![\\p{L}\\p{N}])`, 'u');
|
|
10877
|
+
}
|
|
10878
|
+
/** What a fact looks like when a model writes one — each judged against the owner's facts. */
|
|
10879
|
+
const FACT_SHAPES = [
|
|
10880
|
+
{ kind: 'email', pattern: /[\p{L}\p{N}._%+-]+@[\p{L}\p{N}-]+(?:\.[\p{L}\p{N}-]+)+/gu, key: m => m.toLowerCase() },
|
|
10881
|
+
{ kind: 'phone', pattern: /\+?\d[\d\s().-]{6,}\d/g, key: m => m.replace(/\D/g, '') },
|
|
10882
|
+
{ kind: 'price', pattern: /(?:[$€£₪]\s?\d[\d,.]*|\d[\d,.]*\s?(?:[$€£₪]|ש"ח|NIS|USD|EUR|GBP))/gu, key: m => m.replace(/[^\d]/g, '') },
|
|
10883
|
+
{ kind: 'time', pattern: /(?<!\d)\d{1,2}[:.]\d{2}(?!\d)/g, key: m => m.replace(/\D/g, '') },
|
|
10884
|
+
];
|
|
10885
|
+
const MIN_PHONE_DIGITS = 8;
|
|
10886
|
+
/** "2019 – 2024" is two years, not a phone number. */
|
|
10887
|
+
const YEAR_RANGE = /^\s*\d{4}\s*[-–]\s*\d{4}\s*$/;
|
|
10888
|
+
function ownedKeys(facts) {
|
|
10889
|
+
const keys = new Set();
|
|
10890
|
+
for (const value of Object.values(facts)) {
|
|
10891
|
+
if (typeof value !== 'string')
|
|
10892
|
+
continue;
|
|
10893
|
+
for (const shape of FACT_SHAPES)
|
|
10894
|
+
for (const m of value.match(shape.pattern) ?? [])
|
|
10895
|
+
keys.add(`${shape.kind}:${shape.key(m)}`);
|
|
10896
|
+
}
|
|
10897
|
+
return keys;
|
|
10898
|
+
}
|
|
10899
|
+
/** The words to avoid and the facts never given, in every written text of the plan. */
|
|
10900
|
+
function planWordIssues(plan, ctx) {
|
|
10901
|
+
const issues = [];
|
|
10902
|
+
const avoid = [...new Set(ctx.avoidWords.map(w => w.trim()).filter(Boolean))].map(w => ({ word: w, pattern: wordPattern(w) }));
|
|
10903
|
+
const owned = ownedKeys(ctx.facts);
|
|
10904
|
+
for (const { path, text } of writtenTexts(plan)) {
|
|
10905
|
+
const words = normalise(text);
|
|
10906
|
+
for (const a of avoid)
|
|
10907
|
+
if (a.pattern.test(words))
|
|
10908
|
+
issues.push({ code: PlanWordIssueCode.AvoidWord, path, message: `uses "${a.word}" — a word this site must not use` });
|
|
10909
|
+
for (const shape of FACT_SHAPES) {
|
|
10910
|
+
for (const m of text.match(shape.pattern) ?? []) {
|
|
10911
|
+
const key = shape.key(m);
|
|
10912
|
+
if (shape.kind === 'phone' && (key.length < MIN_PHONE_DIGITS || YEAR_RANGE.test(m)))
|
|
10913
|
+
continue;
|
|
10914
|
+
if (!owned.has(`${shape.kind}:${key}`))
|
|
10915
|
+
issues.push({ code: PlanWordIssueCode.InventedFact, path, message: `"${m.trim()}" looks like a ${shape.kind} the owner never gave — leave it out` });
|
|
10916
|
+
}
|
|
10917
|
+
}
|
|
10918
|
+
}
|
|
10919
|
+
for (const [key, value] of Object.entries(plan.business?.facts ?? {})) {
|
|
10920
|
+
const own = ctx.facts[key];
|
|
10921
|
+
if (!FACT_KEYS.includes(key))
|
|
10922
|
+
continue;
|
|
10923
|
+
if (typeof own !== 'string' || normalise(own).trim() !== normalise(String(value)).trim()) {
|
|
10924
|
+
issues.push({ code: PlanWordIssueCode.FactNotTheOwners, path: `business.facts.${key}`, message: own ? `must be the owner's own words: "${own}"` : 'the owner did not give it — leave it out (it becomes a placeholder)' });
|
|
10925
|
+
}
|
|
10926
|
+
}
|
|
10927
|
+
return issues;
|
|
10928
|
+
}
|
|
10929
|
+
|
|
10125
10930
|
/**
|
|
10126
10931
|
* @vectoriox/iox-ui/generation-core — the shared, FRAMEWORK-FREE site-plan core (design-system plan
|
|
10127
10932
|
* §6b, W1): the Site Plan shape + its validator, the pattern library and the Studio template (both
|
|
@@ -10138,5 +10943,5 @@ var AiCallStatus;
|
|
|
10138
10943
|
* Generated bundle index. Do not edit.
|
|
10139
10944
|
*/
|
|
10140
10945
|
|
|
10141
|
-
export { ACTIVE_BUILD_STATUSES, AI_REFUSAL_NEXT, AI_REFUSAL_STATUS, AI_STEPS, AI_STEP_NEEDS, AI_STEP_TYPICAL_TOKENS, AiCallStatus, AiCapability, AiGenerationStage, AiKeyMode, AiNextKind, AiProviderId, AiRefusalCode, AiStep, BAR_ITEMS_MAX, BAR_ITEMS_MIN, BASELINE_ROLES, BUILD_STEPS, BaselineRole, BindScope, BuildNoticeCode, BuildStep, CREDITS_PER_USD, CardBindTarget, ComposeError, ComposeIssueCode, ContentTypeAction, CreatedKind, DANGLING_LINK_CODES, DEFAULT_PHONE_MENU_BREAKPOINT, ENTRANCE_EASING, ENTRANCE_ORIGIN_LOOK, ENTRANCE_TRIGGER, EntrancePart, FACT_KEYS, FINAL_GENERATION_STAGES, GlobalSlot, KeptReason, NAV_PART_LABELS, NAV_SHAPE_COPY, NEUTRAL_STYLE_PROPS, NODE_BASELINES, NO_TOKENS, NavPagesSource, NavRole, NavShape, NavShapeError, PRICING_PATTERN, PUBLISH_BLOCKING_PLACEHOLDERS, PageDataKind, PathIssueCollector, PatternDirective, PatternGlobalPosition, PatternOpens, PatternPlaceholder, PatternRecipe, PhoneMenuBreakpoint, PlaceholderKind, PlanDataSourceMode, PlanDataSourceType, PreflightRefusalCode, RelativeOp, SAMPLE_IMAGE_REF, SITE_FACT_LABELS, SITE_HEADER_PATTERN, SITE_PATTERNS, SITE_PLAN_VERSION, SITE_TEMPLATES, SPEC_SITE_FACTS, SPEC_SITE_LIBRARY, SPEC_SITE_PATTERNS, SPEC_SITE_PLAN_SHAPE, SPEC_SITE_TEMPLATES, STUDIO_TEMPLATE, SiteBuildKind, SiteBuildStatus, SitePlanError, SitePlanIssueCode, SitePlanSourceKind, SiteTemplateError, SiteTemplateId, StepState, addBarItem, altSourceScopes, assertStyleSlots, assertValidSitePlan, barIconForRoute, blocksPublish, buildKeys, buildStepFacts, builtHomePageId, composeNavHeader, composeSection, composeSite, costUsd, createSerializedNode, creditsOfUsd, describePlanData, entranceAction, entranceInteraction, estimateStepCredits, failedBuildStep, findNavRoot, isActiveBuild, isNonEmptyString, isPlainObject, modelCanDo, moveBarItem, navPages, navShapeBreakpoint, navShapeOf, nextSourceOf, nodeBindPaths, parseSampleImageRef, pathAt, patternById, patternNeedsBinding, patternNodes, patternOptionalParts, patternScopes, patternSlots, patternTakesBinding, phoneMenuPages, planFromTemplate, planNavShape, releasedLayoutDefaults, removeBarItem, requiredShapeKeys, routeParams, sampleImageRef, sectionEntrances, setStyleProps, shapeKeys, siteNavPages, staggerDelay, validateSitePlan, walkSerialized };
|
|
10946
|
+
export { ACTIVE_BUILD_STATUSES, AI_REFUSAL_NEXT, AI_REFUSAL_STATUS, AI_STEPS, AI_STEP_LABELS, AI_STEP_NEEDS, AI_STEP_TYPICAL_TOKENS, AiCallStatus, AiCapability, AiGenerationStage, AiKeyMode, AiNextKind, AiProviderId, AiRefusalCode, AiStep, BAR_ITEMS_MAX, BAR_ITEMS_MIN, BASELINE_ROLES, BUILD_STEPS, BaselineRole, BindScope, BuildNoticeCode, BuildStep, CREDITS_PER_USD, CardBindTarget, ComposeError, ComposeIssueCode, ContentTypeAction, CreatedKind, DANGLING_LINK_CODES, DEFAULT_PHONE_MENU_BREAKPOINT, ENTRANCE_EASING, ENTRANCE_ORIGIN_LOOK, ENTRANCE_TRIGGER, EntrancePart, FACT_KEYS, FINAL_GENERATION_STAGES, GlobalSlot, JSON_SCHEMA_BANNED_KEYWORDS, JsonSchemaShapeError, KeptReason, NAV_PART_LABELS, NAV_SHAPE_COPY, NEUTRAL_STYLE_PROPS, NODE_BASELINES, NO_TOKENS, NavPagesSource, NavRole, NavShape, NavShapeError, PLAN_EDITS_ANSWER_SHAPE, PLAN_EDITS_SCHEMA_NAME, PLAN_EDIT_SHAPE, PRICING_PATTERN, PUBLISH_BLOCKING_PLACEHOLDERS, PageDataKind, PathIssueCollector, PatternDirective, PatternGlobalPosition, PatternOpens, PatternPlaceholder, PatternRecipe, PhoneMenuBreakpoint, PlaceholderKind, PlanChangeAction, PlanChangeKind, PlanDataSourceMode, PlanDataSourceType, PlanEditIssueCode, PlanEditOp, PlanEditsOutcome, PlanWordIssueCode, PreflightRefusalCode, RelativeOp, SAMPLE_IMAGE_REF, SITE_FACT_LABELS, SITE_HEADER_PATTERN, SITE_PATTERNS, SITE_PLAN_SCHEMA_NAME, SITE_PLAN_VERSION, SITE_TEMPLATES, SPEC_SITE_FACTS, SPEC_SITE_LIBRARY, SPEC_SITE_PATTERNS, SPEC_SITE_PLAN_SHAPE, SPEC_SITE_TEMPLATES, STUDIO_TEMPLATE, SiteBuildKind, SiteBuildStatus, SitePlanError, SitePlanIssueCode, SitePlanSourceKind, SitePlanWireKey, SiteTemplateError, SiteTemplateId, StepState, addBarItem, altSourceScopes, applyPlanEdits, assertStyleSlots, assertValidSitePlan, barIconForRoute, blocksPublish, buildKeys, buildStepFacts, builtHomePageId, composeNavHeader, composeSection, composeSite, contentTypeFromWire, contentTypeJsonSchema, costUsd, createSerializedNode, creditsOfUsd, dataSourceJsonSchema, describePlanData, describeShapeKey, entranceAction, entranceInteraction, estimateStepCredits, failedBuildStep, fieldJsonSchema, findNavRoot, isActiveBuild, isNonEmptyString, isPlainObject, jsonAnyOf, jsonBoolean, jsonClosedObject, jsonInteger, jsonListOf, jsonOneOf, jsonSchemaKeywords, jsonSchemaObjects, jsonShapedObject, jsonText, modelCanDo, moveBarItem, navPages, navShapeBreakpoint, navShapeOf, nextSourceOf, nodeBindPaths, pageJsonSchema, parseSampleImageRef, pathAt, patternById, patternNeedsBinding, patternNodes, patternOptionalParts, patternScopes, patternSlots, patternTakesBinding, phoneMenuPages, planEditsFromWire, planEditsJsonSchema, planFromTemplate, planNavShape, planWordIssues, releasedLayoutDefaults, removeBarItem, requiredShapeKeys, routeParams, sampleImageRef, sectionEntrances, sectionFromWire, sectionJsonSchema, sectionPatternIds, setStyleProps, shapeKeys, siteNavPages, sitePlanFromWire, sitePlanJsonSchema, sitePlanToWire, staggerDelay, validatePlanEdits, validateSitePlan, walkSerialized, wordPattern, writtenTexts };
|
|
10142
10947
|
//# sourceMappingURL=vectoriox-iox-ui-generation-core.mjs.map
|