@vectoriox/iox-ui 5.62.1 → 5.64.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.
@@ -1,5 +1,6 @@
1
1
  import { AnimateId, DesignChoices, DesignTokenLike } from '@vectoriox/iox-ui/design-core';
2
2
  import { SeoRawDoc } from '@vectoriox/iox-ui/seo-core';
3
+ import { ContentFieldLike } from '@vectoriox/iox-ui/composite-core';
3
4
 
4
5
  declare const SPEC_SITE_PLAN_SHAPE: {
5
6
  readonly plan: {
@@ -5744,6 +5745,10 @@ declare function buildStepFacts(plan: SitePlan, actions?: Readonly<Record<string
5744
5745
  * - **What the model does not decide is not on the wire** (`PLAN_WIRE_DERIVED`): the look (the owner's pick), the run,
5745
5746
  * the language (the reading's), the facts (the owner's own, verbatim), the version, the home page (the page at `/`).
5746
5747
  * So the format is the same for every studio and every run — one compiled grammar, cached by the provider.
5748
+ * - **What follows from the pages is derived, never asked** (`plan-derived.ts`, vectoriox/iox-cms#49): a per-item page
5749
+ * reading the next item gets its `$siblings.next` source — the structure, asked first and then fixed, cannot know the
5750
+ * pages need it, and the pages cannot add a source. `planToWire` writes every source the plan holds, a derived one
5751
+ * too (it is the plan's: a change's brief shows it, and a plan → wire → plan round trip stays exact).
5747
5752
  *
5748
5753
  * ONE source: every wire key names the plan keys it carries (`refs`) and is described in the plan table's own words, and
5749
5754
  * every plan key is either carried or derived (`planWireGaps()`); a key the plan shape gains breaks the builder until it
@@ -5827,7 +5832,7 @@ type PlanWireParts = Partial<Record<PlanPart, unknown>>;
5827
5832
  * A model's plan (its parts, on the wire) + what the generation decides (the look, the run, the language, the owner's
5828
5833
  * facts) → the Site Plan. Pure. Translates, never repairs: whatever an answer got wrong is still wrong, for
5829
5834
  * `validateSitePlan` — a part that is not an object is the result itself, a part not written yet leaves its keys out
5830
- * (the validator then names them, at their own paths).
5835
+ * (the validator then names them, at their own paths). What follows from the pages alone is added (`withDerivedSources`).
5831
5836
  */
5832
5837
  declare function expandPlanWire(parts: PlanWireParts, context: PlanWireContext): SitePlan | unknown;
5833
5838
  declare function fieldToWire(field: ContentFieldPlan): PlainObject;
@@ -6025,6 +6030,24 @@ declare enum AiCallStatus {
6025
6030
  Interrupted = "interrupted"
6026
6031
  }
6027
6032
 
6033
+ interface RecipeListBinds<T> {
6034
+ picks: T;
6035
+ /** What the fields lack (the recipe's `needs`), or null when they fill it. */
6036
+ gap: string | null;
6037
+ }
6038
+ /** The fields `recipe` binds from a listed type's `fields`, and what they lack. A recipe that lists nothing lacks nothing. */
6039
+ declare function recipeListBinds<T = Readonly<Record<string, string | null>>>(recipe: string | undefined, fields: ReadonlyArray<ContentFieldLike>): RecipeListBinds<T>;
6040
+ /** What a listed type with `fields` lacks for the pattern's recipes that repeat the section's binding — the first gap, or null. */
6041
+ declare function patternListGap(pattern: SitePattern, fields: ReadonlyArray<ContentFieldLike>): string | null;
6042
+
6043
+ /**
6044
+ * The data sources `plan` lacks and its pages need, in page order (appended after the plan's own). Tolerates any
6045
+ * value: what is not a well-formed plan gains nothing, for the validator to judge as written.
6046
+ */
6047
+ declare function derivedDataSources(plan: unknown): DataSourcePlan[];
6048
+ /** `plan` with the data sources its pages need appended (`derivedDataSources`); the same object when it lacks none. */
6049
+ declare function withDerivedSources<T>(plan: T): T;
6050
+
6028
6051
  /**
6029
6052
  * What a structured-output request costs the provider's grammar compiler, counted the way the Claude API counts it
6030
6053
  * (platform.claude.com/docs/en/build-with-claude/structured-outputs, "Schema complexity limits", read 2026-10-27):
@@ -6234,8 +6257,9 @@ declare function validatePlanEdits(edits: unknown): {
6234
6257
  };
6235
6258
  /**
6236
6259
  * Apply a set of edits to a plan — all of them, or none (any issue refuses the set and returns the plan unchanged). The
6237
- * sources only removed pages and sections read are dropped with them; sources nothing read before are kept.
6238
- * The result still has to pass `validateSitePlan`.
6260
+ * sources only removed pages and sections read are dropped with them; sources nothing read before are kept; the sources
6261
+ * the pages now need and no model writes are derived (`derivedDataSources` — a page edit adding `project.next`, as the
6262
+ * plan wire's expansion does, vectoriox/iox-cms#49). The result still has to pass `validateSitePlan`.
6239
6263
  */
6240
6264
  declare function applyPlanEdits(plan: SitePlan, edits: ReadonlyArray<PlanEdit>): PlanEditResult;
6241
6265
  /** Each part's answer format name. */
@@ -6554,5 +6578,5 @@ declare function stockCreditNode(index: number, credit: StockLink): SerializedNo
6554
6578
  /** Append the credit lines to the footer root (once each). Returns how many it added. */
6555
6579
  declare function addStockCredits(footer: SerializedNode, credits: ReadonlyArray<StockLink>): number;
6556
6580
 
6557
- 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 };
6558
- export type { AddBarItemResult, AiEntitlementsView, AiMeter, AiModelEntry, AiRefusal, AiTokenCounts, AiTokenPrice, AuditPage, AuditPlaceholder, BarItemsEditResult, BuildNotice, BuildStepError, BuildStepFacts, BuildStepRecord, CmsDataSourcePlan, ComposeContext, ComposeIssue, ComposedDataSource, ComposedItem, ComposedNavHeader, ComposedPage, ComposedPlaceholder, ComposedRoute, ComposedSection, ComposedSite, ContentFieldPlan, ContentRoles, ContentSeedPlan, ContentTypePlan, CreateNodeOptions, CreatedRecord, DataSourcePlan, DynamicKeysAsList, EntranceAction, EntranceActionOptions, EntranceInteraction, EntranceMark, EntranceSpec, EntranceStamp, FactKey, JsonSchema, NavHeaderInput, NavHeaderOptions, NavPage, NavShapeOptions, NavShapeResult, NodeBaseline, PageArchetype, PageArchetypeId, PageArchetypeSlot, PageData, PageDataLine, PagePlan, PageSectionPlan, PageSeoPlan, PathIssue, PatternManifestEntry, PatternNode, PatternPath, PatternRoleNeed, PlainObject, PlanChange, PlanEdit, PlanEditIssue, PlanEditResult, PlanEditsAnswer, PlanEntrancesInput, PlanWireContext, PlanWireParts, PlanWordContext, PlanWordIssue, PreflightRefusal, PublishRefusal, RelativeDataSourcePlan, SectionEntrances, SectionInput, SectionOptions, SerializedBinding, SerializedNode, SerializedResponsive, ShapeEntry, ShapeIssueCodes, ShowcaseInput, SiteAudit, SiteAuditOptions, SiteAuditSnapshot, SiteBuildRun, SiteBusiness, SiteCheck, SiteFinding, SiteNeedId, SitePageRef, SitePattern, SitePlan, SitePlanIssue, SitePlanSource, SitePlanValidation, SiteTemplate, StockCredit, StockLink, StockPhoto, StockPlacement, StockProviderTerms, StockSlot, StrictSchemaComplexity, StrictSchemaRefusal, TemplatePlanInput, UndoKept, UndoPreview, UndoRecord, WireOptions, WrittenText };
6581
+ 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, derivedDataSources, 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, patternListGap, 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, recipeListBinds, 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, withDerivedSources, wordPattern, writtenTexts };
6582
+ export type { AddBarItemResult, AiEntitlementsView, AiMeter, AiModelEntry, AiRefusal, AiTokenCounts, AiTokenPrice, AuditPage, AuditPlaceholder, BarItemsEditResult, BuildNotice, BuildStepError, BuildStepFacts, BuildStepRecord, CmsDataSourcePlan, ComposeContext, ComposeIssue, ComposedDataSource, ComposedItem, ComposedNavHeader, ComposedPage, ComposedPlaceholder, ComposedRoute, ComposedSection, ComposedSite, ContentFieldPlan, ContentRoles, ContentSeedPlan, ContentTypePlan, CreateNodeOptions, CreatedRecord, DataSourcePlan, DynamicKeysAsList, EntranceAction, EntranceActionOptions, EntranceInteraction, EntranceMark, EntranceSpec, EntranceStamp, FactKey, JsonSchema, NavHeaderInput, NavHeaderOptions, NavPage, NavShapeOptions, NavShapeResult, NodeBaseline, PageArchetype, PageArchetypeId, PageArchetypeSlot, PageData, PageDataLine, PagePlan, PageSectionPlan, PageSeoPlan, PathIssue, PatternManifestEntry, PatternNode, PatternPath, PatternRoleNeed, PlainObject, PlanChange, PlanEdit, PlanEditIssue, PlanEditResult, PlanEditsAnswer, PlanEntrancesInput, PlanWireContext, PlanWireParts, PlanWordContext, PlanWordIssue, PreflightRefusal, PublishRefusal, RecipeListBinds, RelativeDataSourcePlan, SectionEntrances, SectionInput, SectionOptions, SerializedBinding, SerializedNode, SerializedResponsive, ShapeEntry, ShapeIssueCodes, ShowcaseInput, SiteAudit, SiteAuditOptions, SiteAuditSnapshot, SiteBuildRun, SiteBusiness, SiteCheck, SiteFinding, SiteNeedId, SitePageRef, SitePattern, SitePlan, SitePlanIssue, SitePlanSource, SitePlanValidation, SiteTemplate, StockCredit, StockLink, StockPhoto, StockPlacement, StockProviderTerms, StockSlot, StrictSchemaComplexity, StrictSchemaRefusal, TemplatePlanInput, UndoKept, UndoPreview, UndoRecord, WireOptions, WrittenText };
@@ -1,5 +1,5 @@
1
1
  import { MotionId, MoodId, TintId, DensityId, DesignMotion, DesignChoices, DesignMode, StylePreset, ShapeId, AnimateId } from '@vectoriox/iox-ui/design-core';
2
- import { FactKey, PathIssue, JsonSchema, SiteTemplate, ShapeEntry, AiGenerationStage, AiStep, SitePlan, PlanEdit, PlanChange, SiteAudit } from '@vectoriox/iox-ui/generation-core';
2
+ import { FactKey, PathIssue, PlainObject, JsonSchema, SiteTemplate, ShapeEntry, AiGenerationStage, AiStep, SitePlan, PlanEdit, PlanChange, SiteAudit } from '@vectoriox/iox-ui/generation-core';
3
3
  export { JsonSchema } from '@vectoriox/iox-ui/generation-core';
4
4
 
5
5
  declare const SPEC_HARNESS_SOURCE: {
@@ -2577,6 +2577,13 @@ declare function validateSiteAnalysis(value: unknown, ctx: AnalysisContext): Ana
2577
2577
  /** The name a provider shows the format under (`json_schema.name`). */
2578
2578
  declare const SITE_ANALYSIS_SCHEMA_NAME = "site_analysis";
2579
2579
  declare function siteAnalysisJsonSchema(): JsonSchema;
2580
+ /** A model's analysis (the wire) → the document, and the faults only the wire can have (a fact stated twice). Never repairs. */
2581
+ declare function analysisFromWire(wire: unknown): {
2582
+ analysis: unknown;
2583
+ issues: AnalysisIssue[];
2584
+ };
2585
+ /** The document → the wire (what a model writes). */
2586
+ declare function analysisToWire(analysis: SiteAnalysis): PlainObject;
2580
2587
 
2581
2588
  /**
2582
2589
  * What the PLAN prompt is given (AI plan rev 3 §3f "Retrieval, not the whole knowledge base"): the one matched
@@ -2801,7 +2808,7 @@ declare function validateSiteReading(value: unknown, ctx: {
2801
2808
  valid: boolean;
2802
2809
  issues: ReadingIssue[];
2803
2810
  };
2804
- /** A valid reading's answer → `SiteReading` (the analysis alone answers `{ analysis }`). */
2811
+ /** A valid reading's answer → `SiteReading` (the analysis alone answers `{ analysis }`; the facts made a map again). */
2805
2812
  declare function decodeSiteReading(value: unknown, pictureCount: number): SiteReading;
2806
2813
  /** A reference picture as the server read it: its measured palette (and where it is). */
2807
2814
  interface MeasuredPicture {
@@ -2987,5 +2994,5 @@ type SiteAuditView = SiteAudit & {
2987
2994
  advice: LintFinding[];
2988
2995
  };
2989
2996
 
2990
- export { ANIMATE_ADVICE_STEPS, AnalysisIssueCode, AnalysisLocale, AvoidCheck, BUSINESS_TYPES, BUSINESS_TYPE_IDS, BookingNeed, COPY_LENGTH_ORDER, CopyLength, GENERATION_LIMITS, HARNESS_SOURCE, LITTLE_MOVEMENT_AVOID, LITTLE_MOVEMENT_CAP, LintKind, MAX_ANALYSIS_MOODS, MAX_PIXEL_SAMPLES, MAX_READ_PICTURES, NEEDS, NEED_IDS, NeedSource, PALETTE_SIZE, PALETTE_THRESHOLDS, PictureRole, PlanChangeOutcome, PresetFit, RANK_WEIGHTS, READING_SHAPE, READ_ANIMATE, READ_WEIGHTS, RankReason, ReaderState, ReadingIssueCode, SITE_ANALYSIS_SCHEMA_NAME, SITE_READING_SCHEMA_NAME, SPEC_ANALYSIS_SHAPE, SPEC_BUSINESS_TYPES, SPEC_HARNESS_EXAMPLES, SPEC_HARNESS_SOURCE, SPEC_NEEDS, SPEC_STRUCTURES, SPEC_USER_AVOIDS, STRUCTURES, SiteReadRefusal, StyleReadGround, StyleReadHeadings, StyleReadMotion, StyleReadSourceKind, USER_AVOID_IDS, VoiceFormality, VoicePerson, addressesIn, analysisPromptContext, businessTypeById, calmerChoice, decodeSiteReading, inferNeeds, isLookRule, lintDesign, lookMatches, mapStyleRead, matchBusinessType, matchBusinessTypes, measurePalette, mergeStyleReads, motionAdvice, motionFor, normalizeAddress, oklchHue, paletteBrand, paletteGround, paletteSamples, pictureStyleRead, pixelSamples, presetDesign, presetHasSerifHeadings, presetLook, presetReason, rankStylePresets, rankWithReferences, readPalette, referenceSeed, seedPassesPreset, siteAnalysisJsonSchema, siteReadingJsonSchema, startingDesign, structureFor, userAvoidRule, validateSiteAnalysis, validateSiteReading, withoutAddress };
2997
+ export { ANIMATE_ADVICE_STEPS, AnalysisIssueCode, AnalysisLocale, AvoidCheck, BUSINESS_TYPES, BUSINESS_TYPE_IDS, BookingNeed, COPY_LENGTH_ORDER, CopyLength, GENERATION_LIMITS, HARNESS_SOURCE, LITTLE_MOVEMENT_AVOID, LITTLE_MOVEMENT_CAP, LintKind, MAX_ANALYSIS_MOODS, MAX_PIXEL_SAMPLES, MAX_READ_PICTURES, NEEDS, NEED_IDS, NeedSource, PALETTE_SIZE, PALETTE_THRESHOLDS, PictureRole, PlanChangeOutcome, PresetFit, RANK_WEIGHTS, READING_SHAPE, READ_ANIMATE, READ_WEIGHTS, RankReason, ReaderState, ReadingIssueCode, SITE_ANALYSIS_SCHEMA_NAME, SITE_READING_SCHEMA_NAME, SPEC_ANALYSIS_SHAPE, SPEC_BUSINESS_TYPES, SPEC_HARNESS_EXAMPLES, SPEC_HARNESS_SOURCE, SPEC_NEEDS, SPEC_STRUCTURES, SPEC_USER_AVOIDS, STRUCTURES, SiteReadRefusal, StyleReadGround, StyleReadHeadings, StyleReadMotion, StyleReadSourceKind, USER_AVOID_IDS, VoiceFormality, VoicePerson, addressesIn, analysisFromWire, analysisPromptContext, analysisToWire, businessTypeById, calmerChoice, decodeSiteReading, inferNeeds, isLookRule, lintDesign, lookMatches, mapStyleRead, matchBusinessType, matchBusinessTypes, measurePalette, mergeStyleReads, motionAdvice, motionFor, normalizeAddress, oklchHue, paletteBrand, paletteGround, paletteSamples, pictureStyleRead, pixelSamples, presetDesign, presetHasSerifHeadings, presetLook, presetReason, rankStylePresets, rankWithReferences, readPalette, referenceSeed, seedPassesPreset, siteAnalysisJsonSchema, siteReadingJsonSchema, startingDesign, structureFor, userAvoidRule, validateSiteAnalysis, validateSiteReading, withoutAddress };
2991
2998
  export type { AiGenerationView, AnalysisContext, AnalysisIssue, AnalysisValidation, AvoidRule, BusinessType, BusinessTypeMatch, CheckRule, ColourSample, FactRead, GenerationFileView, GenerationReadersView, GenerationStartRequest, GoodFit, LintFinding, LintSite, LookCondition, LookDesign, LookRule, MeasuredPicture, MotionBasis, MotionInput, MotionStart, NeedDef, NeedId, PaletteColour, PaletteReading, PictureStyle, PlanRevisionView, PromptContext, PromptContextOptions, PromptSection, RankInput, RankedPreset, ReadingIssue, ReferenceSeed, SiteAnalysis, SiteAuditView, SiteNeed, SiteReaderView, SiteReading, StartingDesign, StructureChoice, StructureId, StyleHints, StyleRead, UserAvoidId };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectoriox/iox-ui",
3
- "version": "5.62.1",
3
+ "version": "5.64.0",
4
4
  "peerDependencies": {
5
5
  "@angular/common": ">=20.0.0",
6
6
  "@angular/core": ">=20.0.0",
@@ -4,9 +4,9 @@ import { FormPresetDef, FormPresetField, formPresetById } from '@vectoriox/iox-u
4
4
  import {
5
5
  ACCORDION_BIND_PROP, ACCORDION_CONTENT_LIMIT, ACCORDION_WORDS, accordionAnswerOf, accordionItems, accordionQuestionOf, AccordionRole,
6
6
  buildAccordionRecipe, buildCardRecipe, buildCarouselRecipe, buildLightboxRecipe, CardLayout, CardPart, CardRole, CARD_LAYOUTS, CARD_PARTS, CompositeNode,
7
- CompositeNodeSpec, findByRole, LIGHTBOX_LABEL, pickAccordionAutoBinds, pickTabsAutoBinds, SLIDE_TO_ROW, SLIDE_TYPE, buildTabsRecipe, tabLabelNodeOf,
7
+ AccordionAutoBinds, CompositeNodeSpec, findByRole, LIGHTBOX_LABEL, SLIDE_TO_ROW, SLIDE_TYPE, buildTabsRecipe, tabLabelNodeOf,
8
8
  tabPanelImageOf, tabPanelTextOf, tabsListOf, tabsPanels, tabsPanelsOf, tabsTriggers, TABS_CONTENT_LIMIT, TABS_IMAGE_BIND_PROP, TABS_LABEL_BIND_PROP,
9
- TABS_TEXT_BIND_PROP, TABS_WORDS, BadgeTone, BADGE_TONES, buildSegmentedRecipe, CardWords, ITEM_OPEN_STATE, segmentedOptions,
9
+ TABS_TEXT_BIND_PROP, TABS_WORDS, TabsAutoBinds, BadgeTone, BADGE_TONES, buildSegmentedRecipe, CardWords, ITEM_OPEN_STATE, segmentedOptions,
10
10
  } from '@vectoriox/iox-ui/composite-core';
11
11
 
12
12
  import { EntranceMark, EntrancePart, planEntrances } from './entrances';
@@ -14,6 +14,7 @@ import { NODE_BASELINES } from './node-baselines.generated';
14
14
  import {
15
15
  BindScope, CardBindTarget, parsePatternPath, PatternDirective, PatternNode, PatternOpens, PatternPlaceholder, PatternRecipe, SitePattern, patternById,
16
16
  } from './patterns';
17
+ import { recipeListBinds } from './recipe-fields';
17
18
  import { assertStyleSlots, ComposeError, createSerializedNode, SerializedBinding, SerializedNode, setStyleProps, walkSerialized } from './serialized-node';
18
19
  import {
19
20
  ContentRole, ContentTypePlan, DataSourcePlan, DEFAULT_SITE_LOCALE, FactKey, FACT_KEYS, GlobalSlot, isRtlLocale, PagePlan, PageSectionPlan, PlanDataSourceMode,
@@ -838,6 +839,15 @@ function composeSegmented(pn: PatternNode, id: string): SerializedNode {
838
839
  return root;
839
840
  }
840
841
 
842
+ /** The content type a section binding lists, through its data source. */
843
+ function boundContentType(binding: string, ctx: Ctx): ContentTypePlan | undefined {
844
+ const source = ctx.plan.dataSources.find(d => d.alias === binding);
845
+ return source && 'contentType' in source ? ctx.plan.contentTypes.find(t => t.name === source.contentType) : undefined;
846
+ }
847
+
848
+ /** A type's fields as composite-core's pickers read them. */
849
+ const fieldsOf = (type: ContentTypePlan | undefined) => (type?.fields ?? []).map(f => ({ name: f.name, type: f.type, controller: f.controller }));
850
+
841
851
  /**
842
852
  * An Accordion (CC2) — the composite-core recipe stamped with this composer's factory. Manual (no section
843
853
  * binding): three items whose questions and answers are placeholders the studio rewrites (`ph: 'copy'`), never
@@ -864,26 +874,17 @@ function composeAccordion(pn: PatternNode, id: string, ctx: Ctx): SerializedNode
864
874
  }
865
875
  return root;
866
876
  }
867
- const source = ctx.plan.dataSources.find(d => d.alias === binding);
868
- const type = source && 'contentType' in source ? ctx.plan.contentTypes.find(t => t.name === source.contentType) : undefined;
869
- const picks = pickAccordionAutoBinds((type?.fields ?? []).map(f => ({ name: f.name, type: f.type, controller: f.controller })));
870
- if (!picks.question || !picks.answer) {
871
- throw new ComposeError(`${id}: "${binding}" has no short-text field for the question and long-text field for the answer`);
872
- }
877
+ const { picks, gap } = recipeListBinds<AccordionAutoBinds>(pn.recipe, fieldsOf(boundContentType(binding, ctx)));
878
+ if (gap) throw new ComposeError(`${id}: "${binding}" has no ${gap}`);
873
879
  addBinding(root, { prop: 'source', source: binding, path: '' });
874
880
  setProp(root, 'limit', ACCORDION_CONTENT_LIMIT);
875
881
  const [template] = accordionItems(root as CompositeNode);
876
882
  delete template.itemKey;
877
- addBinding(accordionQuestionOf(template) as SerializedNode, { prop: ACCORDION_BIND_PROP, source: binding, path: picks.question });
878
- addBinding(accordionAnswerOf(template) as SerializedNode, { prop: ACCORDION_BIND_PROP, source: binding, path: picks.answer });
883
+ addBinding(accordionQuestionOf(template) as SerializedNode, { prop: ACCORDION_BIND_PROP, source: binding, path: picks.question! });
884
+ addBinding(accordionAnswerOf(template) as SerializedNode, { prop: ACCORDION_BIND_PROP, source: binding, path: picks.answer! });
879
885
  return root;
880
886
  }
881
887
 
882
- /** The content type a section binding lists, through its data source. */
883
- function boundContentType(binding: string, ctx: Ctx): ContentTypePlan | undefined {
884
- const source = ctx.plan.dataSources.find(d => d.alias === binding);
885
- return source && 'contentType' in source ? ctx.plan.contentTypes.find(t => t.name === source.contentType) : undefined;
886
- }
887
888
 
888
889
  const TABS_ROOT_KEY = 'tabs';
889
890
  const TABS_HOLDER_KEYS: ReadonlySet<string> = new Set(['list', 'panels']);
@@ -920,11 +921,8 @@ function composeTabs(pn: PatternNode, id: string, ctx: Ctx): SerializedNode {
920
921
  }
921
922
  return root;
922
923
  }
923
- const type = boundContentType(binding, ctx);
924
- const picks = pickTabsAutoBinds((type?.fields ?? []).map(f => ({ name: f.name, type: f.type, controller: f.controller })));
925
- if (!picks.label || !picks.text) {
926
- throw new ComposeError(`${id}: "${binding}" has no short-text field for the tab and long-text field for its panel`);
927
- }
924
+ const { picks, gap } = recipeListBinds<TabsAutoBinds>(pn.recipe, fieldsOf(boundContentType(binding, ctx)));
925
+ if (gap) throw new ComposeError(`${id}: "${binding}" has no ${gap}`);
928
926
  for (const holder of [tabsListOf(tree), tabsPanelsOf(tree)] as SerializedNode[]) {
929
927
  addBinding(holder, { prop: 'source', source: binding, path: '' });
930
928
  setProp(holder, 'limit', TABS_CONTENT_LIMIT);
@@ -933,12 +931,12 @@ function composeTabs(pn: PatternNode, id: string, ctx: Ctx): SerializedNode {
933
931
  const [panel] = tabsPanels(tree);
934
932
  delete tab.itemKey;
935
933
  delete panel.itemKey;
936
- addBinding(tabLabelNodeOf(tab) as SerializedNode, { prop: TABS_LABEL_BIND_PROP, source: binding, path: picks.label });
937
- addBinding(tabPanelTextOf(panel) as SerializedNode, { prop: TABS_TEXT_BIND_PROP, source: binding, path: picks.text });
934
+ addBinding(tabLabelNodeOf(tab) as SerializedNode, { prop: TABS_LABEL_BIND_PROP, source: binding, path: picks.label! });
935
+ addBinding(tabPanelTextOf(panel) as SerializedNode, { prop: TABS_TEXT_BIND_PROP, source: binding, path: picks.text! });
938
936
  const image = tabPanelImageOf(panel) as SerializedNode | undefined;
939
937
  if (image && picks.image) {
940
938
  addBinding(image, { prop: TABS_IMAGE_BIND_PROP, source: binding, path: picks.image });
941
- addBinding(image, { prop: 'alt', source: binding, path: picks.label });
939
+ addBinding(image, { prop: 'alt', source: binding, path: picks.label! });
942
940
  }
943
941
  return root;
944
942
  }
@@ -0,0 +1,84 @@
1
+ import { BindScope, patternById, patternScopes } from './patterns';
2
+ import { DataSourcePlan, PlanDataSourceMode, PlanDataSourceType, RelativeOp } from './site-plan.model';
3
+ import { isNonEmptyString, isPlainObject, PlainObject } from './shape-check';
4
+ import { nextSourceOf } from './validate-site-plan';
5
+
6
+ /**
7
+ * What a plan reads but no model is asked to write (vectoriox/iox-cms#49): the data sources that follow from the pages
8
+ * alone. A per-item page whose sections read the NEXT item (`project.next`) needs a `$siblings.next` source of its route
9
+ * source — a fact of the pages, while data sources are the plan's STRUCTURE, asked first and fixed once accepted. Asking
10
+ * the structure for it made the pages' answer unfixable by construction (a re-ask of the pages cannot add a source), so
11
+ * it is derived, the same way for the plan wire's expansion (`expandPlanWire`) and for a sentence's page edits
12
+ * (`applyPlanEdits`). Pure and idempotent: a plan that already holds the source gains nothing; a source is derived only
13
+ * where it is valid by construction (its subject a single cms source of a type of the plan), so it never adds an issue
14
+ * at a structure path. Its siblings are walked within an unlimited list of the same type — the structure's first, else a
15
+ * derived one.
16
+ */
17
+
18
+ const NEXT_SUFFIX = 'Next';
19
+ const LIST_SUFFIX = 'All';
20
+
21
+ interface SourceView { type: string; mode?: string; contentType?: string; limit?: unknown; of?: string; op?: string; }
22
+
23
+ /** The sources by alias, as the validator reads them (the first of an alias wins). */
24
+ function sourceViews(list: ReadonlyArray<unknown>): Map<string, SourceView> {
25
+ const views = new Map<string, SourceView>();
26
+ for (const raw of list) {
27
+ if (isPlainObject(raw) && isNonEmptyString(raw['alias']) && !views.has(raw['alias'])) views.set(raw['alias'], raw as unknown as SourceView);
28
+ }
29
+ return views;
30
+ }
31
+
32
+ const readsNext = (page: PlainObject) => Array.isArray(page['sections']) && page['sections'].some(s => {
33
+ const pattern = isPlainObject(s) && typeof s['pattern'] === 'string' ? patternById(s['pattern']) : undefined;
34
+ return !!pattern && patternScopes(pattern).has(BindScope.Next);
35
+ });
36
+
37
+ /** The alias itself, else the first of `<alias>2`, `<alias>3`… no source has. */
38
+ function freeAlias(base: string, taken: ReadonlyMap<string, unknown>): string {
39
+ let alias = base;
40
+ for (let n = 2; taken.has(alias); n++) alias = `${base}${n}`;
41
+ return alias;
42
+ }
43
+
44
+ /** The unlimited list of `contentType` the plan holds (its first), or null. */
45
+ function fullListOf(views: ReadonlyMap<string, SourceView>, contentType: string): string | null {
46
+ for (const [alias, s] of views) {
47
+ if (s.type === PlanDataSourceType.Cms && s.mode === PlanDataSourceMode.Query && s.contentType === contentType && s.limit === undefined) return alias;
48
+ }
49
+ return null;
50
+ }
51
+
52
+ /**
53
+ * The data sources `plan` lacks and its pages need, in page order (appended after the plan's own). Tolerates any
54
+ * value: what is not a well-formed plan gains nothing, for the validator to judge as written.
55
+ */
56
+ export function derivedDataSources(plan: unknown): DataSourcePlan[] {
57
+ if (!isPlainObject(plan) || !Array.isArray(plan['dataSources']) || !Array.isArray(plan['pages']) || !Array.isArray(plan['contentTypes'])) return [];
58
+ const types = new Set(plan['contentTypes'].map(t => (isPlainObject(t) ? t['name'] : undefined)).filter(isNonEmptyString));
59
+ const views = sourceViews(plan['dataSources']);
60
+ const derived: DataSourcePlan[] = [];
61
+ const add = (source: DataSourcePlan) => { derived.push(source); views.set(source.alias, source); };
62
+ for (const page of plan['pages']) {
63
+ if (!isPlainObject(page) || !isNonEmptyString(page['routeSource']) || !readsNext(page)) continue;
64
+ const subject = page['routeSource'];
65
+ const route = views.get(subject);
66
+ if (!route || route.type !== PlanDataSourceType.Cms || route.mode !== PlanDataSourceMode.Single) continue;
67
+ if (!isNonEmptyString(route.contentType) || !types.has(route.contentType) || nextSourceOf(views, subject)) continue;
68
+ let within = fullListOf(views, route.contentType);
69
+ if (!within) {
70
+ within = freeAlias(`${subject}${LIST_SUFFIX}`, views);
71
+ add({ alias: within, type: PlanDataSourceType.Cms, mode: PlanDataSourceMode.Query, contentType: route.contentType });
72
+ }
73
+ add({ alias: freeAlias(`${subject}${NEXT_SUFFIX}`, views), type: PlanDataSourceType.Relative, of: subject, op: RelativeOp.SiblingsNext, within });
74
+ }
75
+ return derived;
76
+ }
77
+
78
+ /** `plan` with the data sources its pages need appended (`derivedDataSources`); the same object when it lacks none. */
79
+ export function withDerivedSources<T>(plan: T): T {
80
+ const derived = derivedDataSources(plan);
81
+ if (!derived.length) return plan;
82
+ const own = plan as unknown as PlainObject;
83
+ return { ...own, dataSources: [...(own['dataSources'] as unknown[]), ...derived] } as T;
84
+ }
@@ -2,6 +2,7 @@ import {
2
2
  jsonAnyOf, jsonInteger, jsonListOf, jsonOneOf, JsonSchema, jsonShapedObject, jsonText,
3
3
  } from './json-schema';
4
4
  import { patternById, patternOptionalParts, patternSlots, patternTakesBinding } from './patterns';
5
+ import { derivedDataSources } from './plan-derived';
5
6
  import {
6
7
  contentTypeToWire, dataSourceToWire, expandWireContentType, expandWireDataSource, expandWireField, expandWirePage, expandWireSection, fieldToWire,
7
8
  pageToWire, PLAN_PARTS, PlanPart, PlanWireShapeError, sectionPatternIds, sectionToWire, wireContentTypeJsonSchema, wireDataSourceJsonSchema,
@@ -367,8 +368,9 @@ const APPLY: Readonly<Record<`${PlanEditOp}`, Apply>> = {
367
368
 
368
369
  /**
369
370
  * Apply a set of edits to a plan — all of them, or none (any issue refuses the set and returns the plan unchanged). The
370
- * sources only removed pages and sections read are dropped with them; sources nothing read before are kept.
371
- * The result still has to pass `validateSitePlan`.
371
+ * sources only removed pages and sections read are dropped with them; sources nothing read before are kept; the sources
372
+ * the pages now need and no model writes are derived (`derivedDataSources` — a page edit adding `project.next`, as the
373
+ * plan wire's expansion does, vectoriox/iox-cms#49). The result still has to pass `validateSitePlan`.
372
374
  */
373
375
  export function applyPlanEdits(plan: SitePlan, edits: ReadonlyArray<PlanEdit>): PlanEditResult {
374
376
  const structure = validatePlanEdits(edits);
@@ -384,8 +386,11 @@ export function applyPlanEdits(plan: SitePlan, edits: ReadonlyArray<PlanEdit>):
384
386
  const added = new Set(changed.filter(ch => ch.kind === PlanChangeKind.DataSource).map(ch => ch.name));
385
387
  const dataSources = draft.dataSources.filter(s => after.has(s.alias) || !before.has(s.alias) || added.has(s.alias));
386
388
  for (const s of draft.dataSources) if (!dataSources.includes(s)) changed.push({ kind: PlanChangeKind.DataSource, action: PlanChangeAction.Removed, name: s.alias });
389
+ const edited = { ...plan, pages: draft.pages, contentTypes: draft.contentTypes, dataSources };
390
+ const derived = derivedDataSources(edited);
391
+ for (const s of derived) changed.push({ kind: PlanChangeKind.DataSource, action: PlanChangeAction.Added, name: s.alias });
387
392
 
388
- return { plan: { ...plan, pages: draft.pages, contentTypes: draft.contentTypes, dataSources }, changed, issues: [] };
393
+ return { plan: derived.length ? { ...edited, dataSources: [...dataSources, ...derived] } : edited, changed, issues: [] };
389
394
  }
390
395
 
391
396
  // ── The format a model writes edits in ─────────────────────────────────────────────────────────────────
@@ -3,6 +3,7 @@ import { FORM_PRESETS } from '@vectoriox/iox-ui/forms-core';
3
3
 
4
4
  import { describeShapeKey, jsonAnyOf, jsonBoolean, jsonInteger, jsonListOf, jsonOneOf, JsonSchema, jsonShapedObject, jsonText } from './json-schema';
5
5
  import { patternLinkSlots, patternOptionalParts, patternSlots, PatternGlobalPosition, SITE_PATTERNS } from './patterns';
6
+ import { withDerivedSources } from './plan-derived';
6
7
  import {
7
8
  ContentFieldPlan, ContentTypePlan, DataSourcePlan, FactKey, GlobalSlot, PAGE_ARCHETYPES, PagePlan, PageSectionPlan, PlanDataSourceMode, PlanDataSourceType,
8
9
  RelativeOp, SITE_NEED_IDS, SITE_PLAN_VERSION, SitePlan, SitePlanSource, SiteLocale,
@@ -30,6 +31,10 @@ import { isPlainObject, PlainObject, ShapeEntry, shapeKeys } from './shape-check
30
31
  * - **What the model does not decide is not on the wire** (`PLAN_WIRE_DERIVED`): the look (the owner's pick), the run,
31
32
  * the language (the reading's), the facts (the owner's own, verbatim), the version, the home page (the page at `/`).
32
33
  * So the format is the same for every studio and every run — one compiled grammar, cached by the provider.
34
+ * - **What follows from the pages is derived, never asked** (`plan-derived.ts`, vectoriox/iox-cms#49): a per-item page
35
+ * reading the next item gets its `$siblings.next` source — the structure, asked first and then fixed, cannot know the
36
+ * pages need it, and the pages cannot add a source. `planToWire` writes every source the plan holds, a derived one
37
+ * too (it is the plan's: a change's brief shows it, and a plan → wire → plan round trip stays exact).
33
38
  *
34
39
  * ONE source: every wire key names the plan keys it carries (`refs`) and is described in the plan table's own words, and
35
40
  * every plan key is either carried or derived (`planWireGaps()`); a key the plan shape gains breaks the builder until it
@@ -91,7 +96,7 @@ const WIRE_KEYS: Readonly<Record<`${PlanWireEntry}`, Readonly<Record<string, Wir
91
96
  plan: {
92
97
  business: inPart(PlanPart.Structure, key('plan.business')),
93
98
  contentTypes: inPart(PlanPart.Structure, key('plan.contentTypes')),
94
- dataSources: inPart(PlanPart.Structure, { refs: ['plan.dataSources'], text: 'dataSource[] — merged by alias into the org library' }),
99
+ dataSources: inPart(PlanPart.Structure, { refs: ['plan.dataSources'], text: "dataSource[] — merged by alias into the org library; a per-item page's $siblings.next source (what a section reading the next item needs) is added for you" }),
95
100
  pages: inPart(PlanPart.Pages, key('plan.pages')),
96
101
  globals: inPart(PlanPart.Pages, key('plan.globals')),
97
102
  },
@@ -465,7 +470,7 @@ export type PlanWireParts = Partial<Record<PlanPart, unknown>>;
465
470
  * A model's plan (its parts, on the wire) + what the generation decides (the look, the run, the language, the owner's
466
471
  * facts) → the Site Plan. Pure. Translates, never repairs: whatever an answer got wrong is still wrong, for
467
472
  * `validateSitePlan` — a part that is not an object is the result itself, a part not written yet leaves its keys out
468
- * (the validator then names them, at their own paths).
473
+ * (the validator then names them, at their own paths). What follows from the pages alone is added (`withDerivedSources`).
469
474
  */
470
475
  export function expandPlanWire(parts: PlanWireParts, context: PlanWireContext): SitePlan | unknown {
471
476
  const written = PLAN_PARTS.filter(part => parts[part] !== undefined).map(part => parts[part]);
@@ -473,7 +478,7 @@ export function expandPlanWire(parts: PlanWireParts, context: PlanWireContext):
473
478
  if (odd !== undefined) return odd;
474
479
  const wire: PlainObject = Object.assign({}, ...(written as PlainObject[]));
475
480
  const { business, contentTypes, dataSources, pages, ...rest } = wire;
476
- return {
481
+ return withDerivedSources({
477
482
  ...rest,
478
483
  version: SITE_PLAN_VERSION,
479
484
  source: { ...context.source },
@@ -483,7 +488,7 @@ export function expandPlanWire(parts: PlanWireParts, context: PlanWireContext):
483
488
  contentTypes: each(contentTypes, expandWireContentType),
484
489
  dataSources: each(dataSources, expandWireDataSource),
485
490
  pages: each(pages, expandWirePage),
486
- };
491
+ });
487
492
  }
488
493
 
489
494
  // ── The plan → the wire ────────────────────────────────────────────────────────────────────────────────────────────
@@ -27,6 +27,8 @@ export * from './plan-data';
27
27
  export * from './showcase';
28
28
  export * from './site-build-run';
29
29
  export * from './ai-run';
30
+ export * from './recipe-fields';
31
+ export * from './plan-derived';
30
32
  export * from './plan-wire';
31
33
  export * from './strict-schema';
32
34
  export * from './plan-edits';
@@ -0,0 +1,49 @@
1
+ import { ContentFieldLike, pickAccordionAutoBinds, pickTabsAutoBinds } from '@vectoriox/iox-ui/composite-core';
2
+
3
+ import { PatternDirective, patternNodes, PatternRecipe, SitePattern } from './patterns';
4
+
5
+ /**
6
+ * The fields a recipe repeating a section's list binds (Accordion, Tabs — R3's From content): composite-core's pickers,
7
+ * the builder switch's rule, decide them. ONE rule for the composer (which binds them) and the gate (which refuses,
8
+ * at the section's `binding`, a list whose type cannot fill the recipe — vectoriox/iox-cms#49: it used to surface only
9
+ * as the composer's error at the plan's root, where the pages part could not tell which section to change).
10
+ */
11
+
12
+ interface ListRecipe {
13
+ picks(fields: ReadonlyArray<ContentFieldLike>): Readonly<Record<string, string | null>>;
14
+ /** The picks the recipe cannot go without. */
15
+ required: ReadonlyArray<string>;
16
+ /** What the listed type must have, in words. */
17
+ needs: string;
18
+ }
19
+
20
+ const LIST_RECIPES: Readonly<Partial<Record<`${PatternRecipe}`, ListRecipe>>> = {
21
+ [PatternRecipe.Accordion]: { picks: f => ({ ...pickAccordionAutoBinds(f) }), required: ['question', 'answer'], needs: 'short-text field for the question and long-text field for the answer' },
22
+ [PatternRecipe.Tabs]: { picks: f => ({ ...pickTabsAutoBinds(f) }), required: ['label', 'text'], needs: 'short-text field for the tab and long-text field for its panel' },
23
+ };
24
+
25
+ export interface RecipeListBinds<T> {
26
+ picks: T;
27
+ /** What the fields lack (the recipe's `needs`), or null when they fill it. */
28
+ gap: string | null;
29
+ }
30
+
31
+ /** The fields `recipe` binds from a listed type's `fields`, and what they lack. A recipe that lists nothing lacks nothing. */
32
+ export function recipeListBinds<T = Readonly<Record<string, string | null>>>(recipe: string | undefined, fields: ReadonlyArray<ContentFieldLike>): RecipeListBinds<T> {
33
+ const spec = recipe ? LIST_RECIPES[recipe as `${PatternRecipe}`] : undefined;
34
+ if (!spec) return { picks: {} as T, gap: null };
35
+ const picks = spec.picks(fields);
36
+ return { picks: picks as unknown as T, gap: spec.required.every(key => picks[key]) ? null : spec.needs };
37
+ }
38
+
39
+ const listsBinding = (src: string | undefined) => src === PatternDirective.Binding || src === PatternDirective.OptionalBinding;
40
+
41
+ /** What a listed type with `fields` lacks for the pattern's recipes that repeat the section's binding — the first gap, or null. */
42
+ export function patternListGap(pattern: SitePattern, fields: ReadonlyArray<ContentFieldLike>): string | null {
43
+ for (const node of patternNodes(pattern)) {
44
+ if (!listsBinding(node.src)) continue;
45
+ const { gap } = recipeListBinds(node.recipe, fields);
46
+ if (gap) return gap;
47
+ }
48
+ return null;
49
+ }
@@ -10,6 +10,7 @@ import {
10
10
  FACT_KEYS, GlobalSlot, PAGE_ARCHETYPES, SITE_NEED_IDS, PlanDataSourceMode, PlanDataSourceType, RelativeOp, REQUIRED_CONTENT_ROLES, SitePlan, SitePlanSourceKind,
11
11
  SITE_LOCALES, SITE_PLAN_VERSION,
12
12
  } from './site-plan.model';
13
+ import { patternListGap } from './recipe-fields';
13
14
  import { SPEC_SITE_PLAN_SHAPE } from './site-spec.generated';
14
15
  import {
15
16
  isNonEmptyString, isPlainObject as isObject, PathIssue, PathIssueCollector, pathAt as at, PlainObject as Obj, requiredShapeKeys, ShapeEntry, shapeKeys,
@@ -187,7 +188,12 @@ function validateDesign(c: Collector, design: unknown): void {
187
188
  if (design['fonts'] !== undefined && !isObject(design['fonts'])) bad('fonts', 'must be an object');
188
189
  }
189
190
 
190
- interface ContentTypeInfo { fields: Set<string>; roles: Record<string, string>; }
191
+ interface ContentTypeInfo {
192
+ fields: Set<string>;
193
+ /** The fields as a recipe's pickers read them (`recipe-fields`). */
194
+ described: Array<{ name: string; type?: string; controller?: string }>;
195
+ roles: Record<string, string>;
196
+ }
191
197
 
192
198
  function validateContentTypes(c: Collector, list: unknown[]): Map<string, ContentTypeInfo> {
193
199
  const types = new Map<string, ContentTypeInfo>();
@@ -223,11 +229,13 @@ function validateContentTypes(c: Collector, list: unknown[]): Map<string, Conten
223
229
  }
224
230
  }
225
231
  if (name && types.has(name)) c.add(SitePlanIssueCode.Duplicate, `${path}.name`, `content type "${name}" is used twice`);
226
- else if (name) types.set(name, { fields: fieldSet, roles });
232
+ else if (name) types.set(name, { fields: fieldSet, described: fields.filter(isObject).map(describeField), roles });
227
233
  });
228
234
  return types;
229
235
  }
230
236
 
237
+ const describeField = (f: Obj) => ({ name: String(f['name']), type: typeof f['type'] === 'string' ? f['type'] : undefined, controller: typeof f['controller'] === 'string' ? f['controller'] : undefined });
238
+
231
239
  /** A content type's roles (L0-3): known roles only, each played by a field of the type; `title` always; `gallery` a list. */
232
240
  function validateRoles(c: Collector, roles: unknown, path: string, typeName: string | null, fields: Set<string>, fieldTypes: Map<unknown, unknown>): Record<string, string> {
233
241
  if (!isObject(roles)) { c.add(SitePlanIssueCode.Shape, path, 'must be an object (which field plays each role)'); return {}; }
@@ -396,6 +404,11 @@ function validatePages(c: Collector, list: unknown[], types: Map<string, Content
396
404
  const src = sources.get(binding);
397
405
  if (!src) c.add(SitePlanIssueCode.UnknownDataSource, `${sp}.binding`, `no data source "${binding}"`);
398
406
  else if (src.type !== PlanDataSourceType.Cms || src.mode !== PlanDataSourceMode.Query) c.add(SitePlanIssueCode.Binding, `${sp}.binding`, `"${binding}" is not a list source`);
407
+ else {
408
+ const listed = src.contentType ? types.get(src.contentType) : undefined;
409
+ const gap = listed ? patternListGap(pattern, listed.described) : null;
410
+ if (gap) c.add(SitePlanIssueCode.Binding, `${sp}.binding`, `"${binding}" lists ${src.contentType}, which has no ${gap} — bind "${patternId}" to a list that has${patternNeedsBinding(pattern) ? '' : ', or to none'}`);
411
+ }
399
412
  }
400
413
  } else if (binding) c.add(SitePlanIssueCode.Binding, `${sp}.binding`, `"${patternId}" repeats nothing — it takes no binding`);
401
414
 
@@ -1,6 +1,6 @@
1
1
  import { MOOD_IDS, MoodId } from '@vectoriox/iox-ui/design-core';
2
2
  import {
3
- describeShapeKey, FACT_KEYS, FactKey, isPlainObject, jsonClosedObject, jsonListOf, jsonOneOf, JsonSchema, jsonShapedObject, jsonText,
3
+ describeShapeKey, FACT_KEYS, FactKey, isPlainObject, jsonListOf, jsonOneOf, JsonSchema, jsonShapedObject, jsonText,
4
4
  PathIssue, PathIssueCollector, pathAt, PlainObject, requiredShapeKeys, ShapeEntry, shapeKeys,
5
5
  } from '@vectoriox/iox-ui/generation-core';
6
6
 
@@ -187,6 +187,11 @@ export function validateSiteAnalysis(value: unknown, ctx: AnalysisContext): Anal
187
187
  * native strict modes accept (Anthropic `output_config.format`, OpenRouter `json_schema` + `strict`): every object
188
188
  * closes with `additionalProperties: false`; no length, count or number bounds (the gate checks those). The
189
189
  * schema is a hint the provider enforces; `validateSiteAnalysis` stays the gate.
190
+ *
191
+ * ONE difference from the document, the plan wire's rule "maps as lists" (vectoriox/iox-cms#47 — Claude refuses a
192
+ * grammar compiled too large, and a map of the five facts, each `{ value, quote }`, was 15 of the reading's 47
193
+ * properties): on the wire `facts` is a list `[{ fact, value, quote }]`, a stated fact once each. `analysisFromWire`
194
+ * makes it the map `validateSiteAnalysis` judges; `analysisToWire` is its inverse (fakes and specs write it).
190
195
  */
191
196
 
192
197
  export type { JsonSchema } from '@vectoriox/iox-ui/generation-core';
@@ -201,19 +206,26 @@ const listOf = jsonListOf;
201
206
  /** An object of `shape`: exactly its keys, its required ones required, each described by the table. */
202
207
  const shaped = (shape: string, properties: Record<string, JsonSchema>): JsonSchema => jsonShapedObject(SHAPE[shape], properties, shape);
203
208
 
204
- /** `{ [fact]: factRead }`: the closed fact keys, each optional — a fact the prompt does not state is left out. */
205
- function factsSchema(factRead: JsonSchema): JsonSchema {
206
- return jsonClosedObject(Object.fromEntries(FACT_KEYS.map(key => [key, factRead])), [], describeShapeKey(SHAPE['analysis'], 'facts'));
207
- }
209
+ /** A stated fact on the wire: which fact, then the document's `factRead` keys in the table's own words. */
210
+ const FACT_LINE_SHAPE: ShapeEntry = {
211
+ fact: `${FACT_KEYS.map(k => `'${k}'`).join(' | ')} — which fact (site.facts), each at most once`,
212
+ ...SHAPE['factRead'],
213
+ };
214
+
215
+ /** The analysis as the wire carries it: `facts` a list of fact lines, described in the table's words. */
216
+ const ANALYSIS_WIRE_SHAPE: ShapeEntry = {
217
+ ...SHAPE['analysis'],
218
+ facts: `factLine[] — ${describeShapeKey(SHAPE['analysis'], 'facts').replace(/^\{ \[fact\]: factRead \} — /, '')}`,
219
+ };
208
220
 
209
221
  export function siteAnalysisJsonSchema(): JsonSchema {
210
- const factRead = shaped('factRead', { value: text(), quote: text() });
211
- return shaped('analysis', {
222
+ const factLine = jsonShapedObject(FACT_LINE_SHAPE, { fact: oneOf(FACT_KEYS), value: text(), quote: text() }, 'factLine');
223
+ return jsonShapedObject(ANALYSIS_WIRE_SHAPE, {
212
224
  businessType: oneOf(BUSINESS_TYPE_IDS),
213
225
  alternatives: listOf(oneOf(BUSINESS_TYPE_IDS)),
214
226
  business: shaped('business', { name: text(), audience: text() }),
215
227
  needs: listOf(shaped('need', { id: oneOf(NEED_IDS), from: oneOf(Object.values(NeedSource)), name: text() })),
216
- facts: factsSchema(factRead),
228
+ facts: listOf(factLine),
217
229
  missingFacts: listOf(oneOf(FACT_KEYS)),
218
230
  voice: shaped('voice', {
219
231
  tone: text(),
@@ -225,5 +237,28 @@ export function siteAnalysisJsonSchema(): JsonSchema {
225
237
  avoid: listOf(oneOf(USER_AVOID_IDS)),
226
238
  hero: shaped('hero', { headline: text(), lead: text() }),
227
239
  locale: oneOf(Object.values(AnalysisLocale)),
240
+ }, 'analysis');
241
+ }
242
+
243
+ /** A model's analysis (the wire) → the document, and the faults only the wire can have (a fact stated twice). Never repairs. */
244
+ export function analysisFromWire(wire: unknown): { analysis: unknown; issues: AnalysisIssue[] } {
245
+ if (!isPlainObject(wire)) return { analysis: wire, issues: [] };
246
+ if (!Array.isArray(wire['facts'])) return { analysis: wire, issues: [{ code: AnalysisIssueCode.Shape, path: 'facts', message: 'must be a list of { fact, value, quote }' }] };
247
+ const facts: PlainObject = {};
248
+ const issues: AnalysisIssue[] = [];
249
+ (wire['facts'] as unknown[]).forEach((line, i) => {
250
+ if (!isPlainObject(line) || typeof line['fact'] !== 'string') {
251
+ issues.push({ code: AnalysisIssueCode.Shape, path: `facts[${i}]`, message: 'must be { fact, value, quote }' });
252
+ return;
253
+ }
254
+ const { fact, ...read } = line;
255
+ if (fact in facts) issues.push({ code: AnalysisIssueCode.Duplicate, path: `facts[${i}].fact`, message: `"${fact}" is stated twice` });
256
+ else facts[fact as string] = read;
228
257
  });
258
+ return { analysis: { ...wire, facts }, issues };
259
+ }
260
+
261
+ /** The document → the wire (what a model writes). */
262
+ export function analysisToWire(analysis: SiteAnalysis): PlainObject {
263
+ return { ...analysis, facts: Object.entries(analysis.facts).map(([fact, read]) => ({ fact, ...read })) };
229
264
  }