@vectoriox/iox-ui 5.66.1 → 5.67.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.
@@ -5617,7 +5617,10 @@ declare enum BuildNoticeCode {
5617
5617
  /** A content type the build reuses is not public in this studio: the site will not show its items. */
5618
5618
  ContentTypeNotPublic = "content-type-not-public",
5619
5619
  /** The stock library could not be read: the picture slots keep the empty box, and the run says so. */
5620
- StockUnavailable = "stock-unavailable"
5620
+ StockUnavailable = "stock-unavailable",
5621
+ /** The stock library was read but holds no photo for this kind of business (never built, or reviewed down to
5622
+ * none): the picture slots keep the empty box, and the run says so (I2.1 — never silently). */
5623
+ StockMissing = "stock-missing"
5621
5624
  }
5622
5625
  interface BuildNotice {
5623
5626
  code: string;
@@ -6482,10 +6485,114 @@ declare function publishVerdict(audit: SiteAudit, acknowledged?: ReadonlyArray<s
6482
6485
  };
6483
6486
 
6484
6487
  /**
6485
- * Stock photos in a built site (AI plan §3g / §3m, I2): a FREE picture from the platform's curated library, copied
6488
+ * How a stock photo is judged against a picture slot (AI plan §3g, I2.1) — pure and deterministic, so the same plan and
6489
+ * the same library always place the same photos. A photo is described by what the library's manifest carries (the
6490
+ * provider's tags, its orientation, the reviewer's subject and notes — each optional, derived when absent); a slot by
6491
+ * its INTENT (what kind of picture its section wants), its aspect ratio and the plan's words around it.
6492
+ */
6493
+ declare enum StockOrientation {
6494
+ Horizontal = "horizontal",
6495
+ Vertical = "vertical",
6496
+ Square = "square"
6497
+ }
6498
+ /** What a photo mainly shows — the reviewer may set it; otherwise it is read from the provider's tags. */
6499
+ declare enum StockSubject {
6500
+ /** A room, a shop front, a garden — somewhere. */
6501
+ Place = "place",
6502
+ /** A close-up: a texture, a tool, an ingredient. */
6503
+ Detail = "detail",
6504
+ /** A thing a business sells or serves: a cake, a bottle, a bouquet. */
6505
+ Product = "product"
6506
+ }
6507
+ /** What a slot wants to show. */
6508
+ declare enum StockIntent {
6509
+ /** The hero / a page header / a call to action: wide and atmospheric. */
6510
+ Atmosphere = "atmosphere",
6511
+ /** A story or a visit section: the place. */
6512
+ Place = "place",
6513
+ /** A feature row: a detail of the work or the service. */
6514
+ Detail = "detail",
6515
+ /** A list item's picture (a product, a service, a dish). */
6516
+ Product = "product"
6517
+ }
6518
+ declare enum StockReviewVerdict {
6519
+ Approved = "approved",
6520
+ Rejected = "rejected"
6521
+ }
6522
+ /** What the review found in a photo that keeps it out of every site (the licence: no person, no logo or brand). */
6523
+ declare enum StockReviewFlag {
6524
+ Person = "person",
6525
+ Logo = "logo"
6526
+ }
6527
+ /** The operator's one review of a photo, as the manifest records it. */
6528
+ interface StockReview {
6529
+ verdict: `${StockReviewVerdict}`;
6530
+ flags?: Array<`${StockReviewFlag}`>;
6531
+ /** The reviewer's own words for it — they count as tags when placing it. */
6532
+ notes?: string;
6533
+ }
6534
+ /** The section families a stock photo may stand in for a picture in, and what each wants. */
6535
+ declare const STOCK_FAMILY_INTENTS: Readonly<Record<string, StockIntent>>;
6536
+ /** Per intent, the subjects it takes, best first. */
6537
+ declare const STOCK_INTENT_SUBJECTS: Readonly<Record<StockIntent, ReadonlyArray<StockSubject>>>;
6538
+ /** The order slots choose in: the hero picks first, a list item last. */
6539
+ declare const STOCK_INTENT_ORDER: ReadonlyArray<StockIntent>;
6540
+ /** The words that tell a photo's subject when the reviewer did not name it (singular, lower case). */
6541
+ declare const STOCK_SUBJECT_WORDS: Readonly<Record<StockSubject, ReadonlyArray<string>>>;
6542
+ /** The share of a photo a slot may crop away and still be a fit: a portrait never fills a 21 / 9 band. */
6543
+ declare const STOCK_MIN_KEPT = 0.5;
6544
+ /**
6545
+ * A word as compared — a canonical form, never shown: lower case, a plain plural made singular, and `-ie` / `-y` one
6546
+ * ending (English cannot tell `cookies` from `pastries` by rule, so `cookie`, `cookies`, `pastry`, `pastries` all
6547
+ * fold to the same shape as their singular).
6548
+ */
6549
+ declare function stockWord(raw: string): string;
6550
+ /** The meaningful words of a text, in order, each once. */
6551
+ declare function stockWords(...texts: ReadonlyArray<unknown>): string[];
6552
+ /** A CSS aspect ratio (`4 / 3`, `1.5`) as width / height, or undefined. */
6553
+ declare function aspectRatioOf(aspect: unknown): number | undefined;
6554
+ /** What the planner reads of a library photo. */
6555
+ interface StockPhotoTraits {
6556
+ alt: string;
6557
+ width?: number;
6558
+ height?: number;
6559
+ tags?: ReadonlyArray<string>;
6560
+ orientation?: `${StockOrientation}`;
6561
+ subject?: `${StockSubject}`;
6562
+ review?: StockReview;
6563
+ }
6564
+ /** A photo may be placed: not rejected and nothing flagged by the review. */
6565
+ declare function stockPhotoUsable(photo: Pick<StockPhotoTraits, 'review'>): boolean;
6566
+ /** The words a photo is placed by: its tags (else its alt — the provider's own words) and the reviewer's notes. */
6567
+ declare function stockPhotoWords(photo: Pick<StockPhotoTraits, 'alt' | 'tags' | 'review'>): string[];
6568
+ /** Its width / height: the size when known, else its orientation's nominal shape, else undefined. */
6569
+ declare function stockPhotoRatio(photo: Pick<StockPhotoTraits, 'width' | 'height' | 'orientation'>): number | undefined;
6570
+ /** The orientation of a width / height ratio. */
6571
+ declare function orientationOfRatio(ratio: number): StockOrientation;
6572
+ /** The reviewer's subject, else the one its words name most (ties: the enum's order), else undefined. */
6573
+ declare function stockPhotoSubject(photo: Pick<StockPhotoTraits, 'alt' | 'tags' | 'review' | 'subject'>): StockSubject | undefined;
6574
+ /** What a slot asks of a photo. */
6575
+ interface StockNeed {
6576
+ intent: `${StockIntent}`;
6577
+ /** The slot's width / height (undefined: any shape). */
6578
+ aspect?: number;
6579
+ /** The plan's words around the slot. */
6580
+ words: ReadonlyArray<string>;
6581
+ }
6582
+ /** The share of the photo a slot shows (1 = the same shape), or undefined when either shape is unknown. */
6583
+ declare function stockKept(photo: Pick<StockPhotoTraits, 'width' | 'height' | 'orientation'>, aspect: number | undefined): number | undefined;
6584
+ /** A photo fits a slot when the slot keeps at least half of it (an unknown shape fits). */
6585
+ declare function stockFits(photo: Pick<StockPhotoTraits, 'width' | 'height' | 'orientation'>, need: Pick<StockNeed, 'aspect'>): boolean;
6586
+ /** How well a photo answers a slot: its subject for the intent, its shape, the plan's words and the type's imagery. */
6587
+ declare function scoreStockPhoto(photo: StockPhotoTraits, need: StockNeed, imagery?: ReadonlyArray<string>): number;
6588
+
6589
+ /**
6590
+ * Stock photos in a built site (AI plan §3g / §3m, I2 + I2.1): a FREE picture from the platform's curated library, copied
6486
6591
  * into the studio's own storage (never hot-linked), placed only in an EMPTY picture slot of a section that may show
6487
6592
  * one — never the owner's work (a collection, a detail page, a gallery: what the nine portfolio types sell is theirs),
6488
- * never a person (testimonials, the team), never anywhere when the business type says `stock: false`. Its alt text is
6593
+ * never a person (testimonials, the team), never anywhere when the business type says `stock: false` — and, since I2.1
6594
+ * (Doron, 2026-10-28), in the picture of a MARKED sample item (a product, a service, a dish) of a type that may show
6595
+ * stock. Which photo goes where is chosen by what the slot is about (`stock-match.ts`). Its alt text is
6489
6596
  * the provider's description of what it shows (never a claim), and the credit line its licence requires — when it
6490
6597
  * requires one — is placed in the footer. Pure: the build step does the copying.
6491
6598
  */
@@ -6528,34 +6635,77 @@ interface StockCredit {
6528
6635
  licence: string;
6529
6636
  licenceUrl: string;
6530
6637
  }
6531
- /** One picture of the platform library (`iox-stock/<type>/`), as its manifest lists it. */
6532
- interface StockPhoto {
6533
- /** The library key (`spa/12345.jpg`) — the copy's file name is derived from it. */
6638
+ /**
6639
+ * One picture of the platform library (`iox-stock/<type>/`), as its manifest lists it. Everything past `credit` is
6640
+ * optional (I2.1) — a manifest written before it is read as it was, and the planner derives what is missing.
6641
+ */
6642
+ interface StockPhoto extends StockPhotoTraits {
6643
+ /** The library key (`spa/12345.jpg`) — the copy's file name is derived from it, the build reads it from the library's address. */
6534
6644
  key: string;
6535
- /** Where the build downloads it from (the platform bucket). */
6645
+ /** Where the operator's run wrote it (kept for older readers; a build resolves `key` against the library's address). */
6536
6646
  url: string;
6537
6647
  /** What it shows, in the provider's words — the alt text. Never a claim. */
6538
6648
  alt: string;
6539
- width?: number;
6540
- height?: number;
6541
6649
  credit: StockCredit;
6542
6650
  }
6543
6651
  /** The section families a stock photo may stand in for a picture in. */
6544
6652
  declare const STOCK_FAMILIES: ReadonlySet<string>;
6545
- interface StockSlot {
6653
+ /** The aspect a list item's picture is shown at (Card v2's Image top, `composite-core` card layouts). */
6654
+ declare const STOCK_ITEM_ASPECT: number;
6655
+ declare enum StockSlotKind {
6656
+ /** An empty Image of a composed section. */
6657
+ Section = "section",
6658
+ /** The image field of a marked sample item. */
6659
+ Item = "item"
6660
+ }
6661
+ /** An empty picture of a composed section a stock photo may fill. */
6662
+ interface StockSlot extends StockNeed {
6663
+ kind: `${StockSlotKind.Section}`;
6546
6664
  /** The Image node the photo goes into. */
6547
6665
  nodeId: string;
6548
6666
  /** The page key it lives on. */
6549
6667
  owner: string;
6550
6668
  pattern: string;
6551
6669
  }
6552
- /** The empty picture slots of a composed site a stock photo may fill, in page and section order. */
6670
+ /** The image field of a sample item (a product, a service, a dish) a stock photo may fill. */
6671
+ interface StockItemSlot extends StockNeed {
6672
+ kind: `${StockSlotKind.Item}`;
6673
+ contentType: string;
6674
+ /** The item's 0-based position in the content type's samples. */
6675
+ index: number;
6676
+ /** The field playing the type's `image` role. */
6677
+ field: string;
6678
+ }
6679
+ type StockTarget = StockSlot | StockItemSlot;
6680
+ /** Where a photo may not appear twice: one page; one content type's samples (a list shows them together). */
6681
+ declare function stockGroupOf(target: StockTarget): string;
6682
+ /**
6683
+ * The empty picture slots of a composed site a stock photo may fill, in page and section order — each with its intent
6684
+ * (the section's family), its aspect, and the words around it: the text beside it (its row), the section's slots and
6685
+ * the page's name and search words.
6686
+ */
6553
6687
  declare function stockSlots(plan: SitePlan, composed: Pick<ComposedSite, 'pages' | 'placeholders'>): StockSlot[];
6554
- interface StockPlacement extends StockSlot {
6688
+ /**
6689
+ * The sample items' pictures a stock photo may fill: per content type with an `image` role, each MARKED sample whose
6690
+ * image is still a bundled sample (`{sampleImage:k}`), with the type's and the item's own words.
6691
+ */
6692
+ declare function stockItemSlots(plan: Pick<SitePlan, 'contentTypes'>, composed: Pick<ComposedSite, 'items'>): StockItemSlot[];
6693
+ type StockPlacement<T extends StockTarget = StockTarget> = T & {
6555
6694
  photo: StockPhoto;
6695
+ };
6696
+ /** What a placement run is told besides the slots and the library. */
6697
+ interface StockPlanOptions {
6698
+ /** The business type's imagery line (harness-core `BusinessType.imagery`) — its words count for every slot. */
6699
+ imagery?: string;
6556
6700
  }
6557
- /** Which photo goes where: the library's photos in order, one per slot, never twice; none when the type says no stock. */
6558
- declare function planStockPhotos(slots: ReadonlyArray<StockSlot>, photos: ReadonlyArray<StockPhoto>, stockAllowed: boolean): StockPlacement[];
6701
+ /**
6702
+ * Which photo goes where (I2.1). Slots choose in intent order (the hero first, list items last; page order within),
6703
+ * each taking the best-scoring photo that FITS its shape (`stockFits`) and is not already on its page (or among its
6704
+ * type's samples) — a photo no slot has taken yet always before one another page shows. Ties go to the library's
6705
+ * order. A slot nothing fits keeps its empty box (a sample its abstract picture). None at all when the type says no
6706
+ * stock, and never a photo the review rejected or flagged. Returned in the slots' order.
6707
+ */
6708
+ declare function planStockPhotos<T extends StockTarget>(slots: ReadonlyArray<T>, photos: ReadonlyArray<StockPhoto>, stockAllowed: boolean, opts?: StockPlanOptions): Array<StockPlacement<T>>;
6559
6709
  /** A placed photo's copy in the studio's storage: the key the build writes it under. */
6560
6710
  declare function stockCopyKey(photo: Pick<StockPhoto, 'key'>): string;
6561
6711
  /** Put the studio's copy of each photo into its slot (src + the provider's alt). Returns the nodes changed. */
@@ -6564,6 +6714,13 @@ declare function applyStockPhotos(trees: ReadonlyArray<SerializedNode>, placed:
6564
6714
  url: string;
6565
6715
  alt: string;
6566
6716
  }>): number;
6717
+ /** Put the studio's copy of each photo into its sample item's image field. Returns the items changed. */
6718
+ declare function applyStockItemPhotos(items: Readonly<Record<string, ReadonlyArray<Record<string, unknown>>>>, placed: ReadonlyArray<{
6719
+ contentType: string;
6720
+ index: number;
6721
+ field: string;
6722
+ url: string;
6723
+ }>): number;
6567
6724
  /** The line a picture's licence requires on the site, or null when it requires none (`STOCK_PROVIDER_TERMS`). */
6568
6725
  declare function stockCreditLine(credit: Pick<StockCredit, 'provider'>): StockLink | null;
6569
6726
  /** The credit lines a set of photos needs — one per distinct line, none for a provider whose licence asks none. */
@@ -6578,5 +6735,5 @@ declare function stockCreditNode(index: number, credit: StockLink): SerializedNo
6578
6735
  /** Append the credit lines to the footer root (once each). Returns how many it added. */
6579
6736
  declare function addStockCredits(footer: SerializedNode, credits: ReadonlyArray<StockLink>): number;
6580
6737
 
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 };
6738
+ 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_FAMILY_INTENTS, STOCK_INTENT_ORDER, STOCK_INTENT_SUBJECTS, STOCK_ITEM_ASPECT, STOCK_MIN_KEPT, STOCK_PROVIDER_TERMS, STOCK_SUBJECT_WORDS, STRICT_SCHEMA_BUDGET, STRICT_SCHEMA_LIMITS, STUDIO_TEMPLATE, SiteBuildKind, SiteBuildStatus, SiteCheckId, SiteFindingCode, SiteLocale, SitePlanError, SitePlanIssueCode, SitePlanSourceKind, SiteTemplateError, SiteTemplateId, StepState, StockIntent, StockOrientation, StockProviderId, StockReviewFlag, StockReviewVerdict, StockSlotKind, StockSubject, addBarItem, addStockCredits, aiStepParts, applyPlanEdits, applyStockItemPhotos, applyStockPhotos, archetypeIssues, aspectRatioOf, 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, orientationOfRatio, 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, scoreStockPhoto, sectionEntrances, sectionPatternIds, sectionToWire, setStyleProps, shapeKeys, showcaseGlobals, showcasePages, showcasePlans, siteNavPages, slotBudget, stockCopyKey, stockCreditLine, stockCreditLines, stockCreditNode, stockCreditNodeId, stockFits, stockGroupOf, stockItemSlots, stockKept, stockPhotoRatio, stockPhotoSubject, stockPhotoUsable, stockPhotoWords, stockSlots, stockWord, stockWords, strictSchemaComplexity, strictSchemaRefusal, typicalStepTokens, validatePlanEdits, validateSitePlan, walkSerialized, wireContentTypeJsonSchema, wireDataSourceJsonSchema, wireFieldJsonSchema, wirePageJsonSchema, wireSectionJsonSchema, withDerivedSources, wordPattern, writtenTexts };
6739
+ 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, StockItemSlot, StockLink, StockNeed, StockPhoto, StockPhotoTraits, StockPlacement, StockPlanOptions, StockProviderTerms, StockReview, StockSlot, StockTarget, StrictSchemaComplexity, StrictSchemaRefusal, TemplatePlanInput, UndoKept, UndoPreview, UndoRecord, WireOptions, WrittenText };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectoriox/iox-ui",
3
- "version": "5.66.1",
3
+ "version": "5.67.0",
4
4
  "peerDependencies": {
5
5
  "@angular/common": ">=20.0.0",
6
6
  "@angular/core": ">=20.0.0",
@@ -34,4 +34,5 @@ export * from './strict-schema';
34
34
  export * from './plan-edits';
35
35
  export * from './plan-words';
36
36
  export * from './audit-site';
37
+ export * from './stock-match';
37
38
  export * from './stock-photos';
@@ -126,6 +126,9 @@ export enum BuildNoticeCode {
126
126
  ContentTypeNotPublic = 'content-type-not-public',
127
127
  /** The stock library could not be read: the picture slots keep the empty box, and the run says so. */
128
128
  StockUnavailable = 'stock-unavailable',
129
+ /** The stock library was read but holds no photo for this kind of business (never built, or reviewed down to
130
+ * none): the picture slots keep the empty box, and the run says so (I2.1 — never silently). */
131
+ StockMissing = 'stock-missing',
129
132
  }
130
133
 
131
134
  export interface BuildNotice {
@@ -0,0 +1,238 @@
1
+ /**
2
+ * How a stock photo is judged against a picture slot (AI plan §3g, I2.1) — pure and deterministic, so the same plan and
3
+ * the same library always place the same photos. A photo is described by what the library's manifest carries (the
4
+ * provider's tags, its orientation, the reviewer's subject and notes — each optional, derived when absent); a slot by
5
+ * its INTENT (what kind of picture its section wants), its aspect ratio and the plan's words around it.
6
+ */
7
+
8
+ export enum StockOrientation {
9
+ Horizontal = 'horizontal',
10
+ Vertical = 'vertical',
11
+ Square = 'square',
12
+ }
13
+
14
+ /** What a photo mainly shows — the reviewer may set it; otherwise it is read from the provider's tags. */
15
+ export enum StockSubject {
16
+ /** A room, a shop front, a garden — somewhere. */
17
+ Place = 'place',
18
+ /** A close-up: a texture, a tool, an ingredient. */
19
+ Detail = 'detail',
20
+ /** A thing a business sells or serves: a cake, a bottle, a bouquet. */
21
+ Product = 'product',
22
+ }
23
+
24
+ /** What a slot wants to show. */
25
+ export enum StockIntent {
26
+ /** The hero / a page header / a call to action: wide and atmospheric. */
27
+ Atmosphere = 'atmosphere',
28
+ /** A story or a visit section: the place. */
29
+ Place = 'place',
30
+ /** A feature row: a detail of the work or the service. */
31
+ Detail = 'detail',
32
+ /** A list item's picture (a product, a service, a dish). */
33
+ Product = 'product',
34
+ }
35
+
36
+ export enum StockReviewVerdict {
37
+ Approved = 'approved',
38
+ Rejected = 'rejected',
39
+ }
40
+
41
+ /** What the review found in a photo that keeps it out of every site (the licence: no person, no logo or brand). */
42
+ export enum StockReviewFlag {
43
+ Person = 'person',
44
+ Logo = 'logo',
45
+ }
46
+
47
+ /** The operator's one review of a photo, as the manifest records it. */
48
+ export interface StockReview {
49
+ verdict: `${StockReviewVerdict}`;
50
+ flags?: Array<`${StockReviewFlag}`>;
51
+ /** The reviewer's own words for it — they count as tags when placing it. */
52
+ notes?: string;
53
+ }
54
+
55
+ /** The section families a stock photo may stand in for a picture in, and what each wants. */
56
+ export const STOCK_FAMILY_INTENTS: Readonly<Record<string, StockIntent>> = {
57
+ hero: StockIntent.Atmosphere,
58
+ header: StockIntent.Atmosphere,
59
+ cta: StockIntent.Atmosphere,
60
+ story: StockIntent.Place,
61
+ contact: StockIntent.Place,
62
+ feature: StockIntent.Detail,
63
+ content: StockIntent.Detail,
64
+ };
65
+
66
+ /** Per intent, the subjects it takes, best first. */
67
+ export const STOCK_INTENT_SUBJECTS: Readonly<Record<StockIntent, ReadonlyArray<StockSubject>>> = {
68
+ [StockIntent.Atmosphere]: [StockSubject.Place, StockSubject.Detail, StockSubject.Product],
69
+ [StockIntent.Place]: [StockSubject.Place, StockSubject.Detail, StockSubject.Product],
70
+ [StockIntent.Detail]: [StockSubject.Detail, StockSubject.Product, StockSubject.Place],
71
+ [StockIntent.Product]: [StockSubject.Product, StockSubject.Detail, StockSubject.Place],
72
+ };
73
+
74
+ /** The order slots choose in: the hero picks first, a list item last. */
75
+ export const STOCK_INTENT_ORDER: ReadonlyArray<StockIntent> = [StockIntent.Atmosphere, StockIntent.Place, StockIntent.Detail, StockIntent.Product];
76
+
77
+ /** The words that tell a photo's subject when the reviewer did not name it (singular, lower case). */
78
+ export const STOCK_SUBJECT_WORDS: Readonly<Record<StockSubject, ReadonlyArray<string>>> = {
79
+ [StockSubject.Place]: [
80
+ 'room', 'interior', 'studio', 'salon', 'shop', 'store', 'boutique', 'restaurant', 'cafe', 'bar', 'bakery', 'cellar', 'kitchen',
81
+ 'gym', 'clinic', 'office', 'lobby', 'hall', 'building', 'architecture', 'facade', 'exterior', 'street', 'garden', 'terrace',
82
+ 'patio', 'pool', 'sauna', 'window', 'landscape', 'view', 'beach', 'forest', 'mountain', 'city', 'house', 'workshop', 'counter',
83
+ 'shelf', 'reception', 'spa', 'hotel', 'bedroom', 'bathroom', 'living', 'courtyard', 'vineyard', 'market',
84
+ ],
85
+ [StockSubject.Detail]: [
86
+ 'texture', 'detail', 'closeup', 'macro', 'pattern', 'fabric', 'wood', 'wooden', 'stone', 'marble', 'leaf', 'towel', 'tool',
87
+ 'equipment', 'ingredient', 'spice', 'grain', 'bean', 'oil', 'water', 'steam', 'shadow', 'paper', 'thread', 'brush', 'ceramic',
88
+ ],
89
+ [StockSubject.Product]: [
90
+ 'product', 'bottle', 'wine', 'beer', 'coffee', 'cup', 'mug', 'tea', 'cake', 'bread', 'pastry', 'croissant', 'cookie', 'dessert',
91
+ 'chocolate', 'food', 'dish', 'plate', 'meal', 'pizza', 'salad', 'soup', 'burger', 'fruit', 'flower', 'bouquet', 'jewelry',
92
+ 'jewellery', 'ring', 'necklace', 'cosmetic', 'cream', 'soap', 'perfume', 'candle', 'shoe', 'bag', 'dress', 'clothing', 'book', 'toy',
93
+ ],
94
+ };
95
+
96
+ /** Words that say nothing about what a picture shows (the plan's sample and placeholder words among them). */
97
+ const STOP_WORDS: ReadonlySet<string> = new Set([
98
+ 'the', 'and', 'with', 'for', 'your', 'our', 'you', 'from', 'into', 'this', 'that', 'here', 'there', 'about', 'page', 'section',
99
+ 'sample', 'replace', 'image', 'photo', 'picture', 'sentence', 'words', 'own', 'how', 'what', 'who', 'are', 'all', 'one', 'two',
100
+ 'three', 'more', 'less', 'very', 'tell', 'story', 'item', 'service', 'project',
101
+ ]);
102
+ const MIN_WORD = 3;
103
+ const HTML_TAG = /<[^>]*>/g;
104
+ const WORD_SPLIT = /[^\p{L}\p{N}]+/u;
105
+
106
+ /** Nominal ratios (width / height) of an orientation when the manifest gives no size. */
107
+ const ORIENTATION_RATIO: Readonly<Record<StockOrientation, number>> = {
108
+ [StockOrientation.Horizontal]: 3 / 2,
109
+ [StockOrientation.Vertical]: 2 / 3,
110
+ [StockOrientation.Square]: 1,
111
+ };
112
+ /** The share of a photo a slot may crop away and still be a fit: a portrait never fills a 21 / 9 band. */
113
+ export const STOCK_MIN_KEPT = 0.5;
114
+ /** What an unknown shape keeps, for scoring only (neither rewarded nor refused). */
115
+ const UNKNOWN_KEPT = 0.75;
116
+
117
+ /** Score weights — subject, then shape, then the plan's own words, then the business type's imagery words. */
118
+ const SUBJECT_POINTS: ReadonlyArray<number> = [4, 2, 1];
119
+ const UNKNOWN_SUBJECT_POINTS = 1.5;
120
+ const SHAPE_POINTS = 4;
121
+ const WORD_POINTS = 2;
122
+ const MAX_WORD_MATCHES = 3;
123
+ const IMAGERY_POINTS = 1;
124
+ const MAX_IMAGERY_MATCHES = 2;
125
+
126
+ /**
127
+ * A word as compared — a canonical form, never shown: lower case, a plain plural made singular, and `-ie` / `-y` one
128
+ * ending (English cannot tell `cookies` from `pastries` by rule, so `cookie`, `cookies`, `pastry`, `pastries` all
129
+ * fold to the same shape as their singular).
130
+ */
131
+ export function stockWord(raw: string): string {
132
+ let w = raw.toLowerCase();
133
+ if (w.length > 3 && w.endsWith('s') && !w.endsWith('ss') && !STOP_WORDS.has(w)) w = w.slice(0, -1);
134
+ return w.length > 3 && w.endsWith('ie') ? `${w.slice(0, -2)}y` : w;
135
+ }
136
+
137
+ /** The meaningful words of a text, in order, each once. */
138
+ export function stockWords(...texts: ReadonlyArray<unknown>): string[] {
139
+ const out = new Set<string>();
140
+ for (const text of texts) {
141
+ if (typeof text !== 'string') continue;
142
+ for (const raw of text.replace(HTML_TAG, ' ').split(WORD_SPLIT)) {
143
+ const w = stockWord(raw);
144
+ if (w.length >= MIN_WORD && !STOP_WORDS.has(w) && !/^\d+$/.test(w)) out.add(w);
145
+ }
146
+ }
147
+ return [...out];
148
+ }
149
+
150
+ /** A CSS aspect ratio (`4 / 3`, `1.5`) as width / height, or undefined. */
151
+ export function aspectRatioOf(aspect: unknown): number | undefined {
152
+ if (typeof aspect === 'number') return aspect > 0 ? aspect : undefined;
153
+ if (typeof aspect !== 'string') return undefined;
154
+ const [w, h] = aspect.split('/').map(s => Number(s.trim()));
155
+ const ratio = h === undefined ? w : w / h;
156
+ return Number.isFinite(ratio) && ratio > 0 ? ratio : undefined;
157
+ }
158
+
159
+ /** What the planner reads of a library photo. */
160
+ export interface StockPhotoTraits {
161
+ alt: string;
162
+ width?: number;
163
+ height?: number;
164
+ tags?: ReadonlyArray<string>;
165
+ orientation?: `${StockOrientation}`;
166
+ subject?: `${StockSubject}`;
167
+ review?: StockReview;
168
+ }
169
+
170
+ /** A photo may be placed: not rejected and nothing flagged by the review. */
171
+ export function stockPhotoUsable(photo: Pick<StockPhotoTraits, 'review'>): boolean {
172
+ return photo.review?.verdict !== StockReviewVerdict.Rejected && !(photo.review?.flags?.length);
173
+ }
174
+
175
+ /** The words a photo is placed by: its tags (else its alt — the provider's own words) and the reviewer's notes. */
176
+ export function stockPhotoWords(photo: Pick<StockPhotoTraits, 'alt' | 'tags' | 'review'>): string[] {
177
+ const tags = photo.tags?.length ? photo.tags.join(', ') : photo.alt;
178
+ return stockWords(tags, photo.review?.notes);
179
+ }
180
+
181
+ /** Its width / height: the size when known, else its orientation's nominal shape, else undefined. */
182
+ export function stockPhotoRatio(photo: Pick<StockPhotoTraits, 'width' | 'height' | 'orientation'>): number | undefined {
183
+ if (photo.width && photo.height && photo.width > 0 && photo.height > 0) return photo.width / photo.height;
184
+ return photo.orientation ? ORIENTATION_RATIO[photo.orientation as StockOrientation] : undefined;
185
+ }
186
+
187
+ /** The orientation of a width / height ratio. */
188
+ export function orientationOfRatio(ratio: number): StockOrientation {
189
+ if (ratio > 1.05) return StockOrientation.Horizontal;
190
+ return ratio < 0.95 ? StockOrientation.Vertical : StockOrientation.Square;
191
+ }
192
+
193
+ /** The reviewer's subject, else the one its words name most (ties: the enum's order), else undefined. */
194
+ export function stockPhotoSubject(photo: Pick<StockPhotoTraits, 'alt' | 'tags' | 'review' | 'subject'>): StockSubject | undefined {
195
+ if (photo.subject && (Object.values(StockSubject) as string[]).includes(photo.subject)) return photo.subject as StockSubject;
196
+ const words = new Set(stockPhotoWords(photo));
197
+ let best: StockSubject | undefined;
198
+ let most = 0;
199
+ for (const subject of Object.values(StockSubject)) {
200
+ const n = STOCK_SUBJECT_WORDS[subject].filter(w => words.has(stockWord(w))).length;
201
+ if (n > most) { most = n; best = subject; }
202
+ }
203
+ return best;
204
+ }
205
+
206
+ /** What a slot asks of a photo. */
207
+ export interface StockNeed {
208
+ intent: `${StockIntent}`;
209
+ /** The slot's width / height (undefined: any shape). */
210
+ aspect?: number;
211
+ /** The plan's words around the slot. */
212
+ words: ReadonlyArray<string>;
213
+ }
214
+
215
+ /** The share of the photo a slot shows (1 = the same shape), or undefined when either shape is unknown. */
216
+ export function stockKept(photo: Pick<StockPhotoTraits, 'width' | 'height' | 'orientation'>, aspect: number | undefined): number | undefined {
217
+ const ratio = stockPhotoRatio(photo);
218
+ if (!ratio || !aspect) return undefined;
219
+ return Math.min(ratio, aspect) / Math.max(ratio, aspect);
220
+ }
221
+
222
+ /** A photo fits a slot when the slot keeps at least half of it (an unknown shape fits). */
223
+ export function stockFits(photo: Pick<StockPhotoTraits, 'width' | 'height' | 'orientation'>, need: Pick<StockNeed, 'aspect'>): boolean {
224
+ const kept = stockKept(photo, need.aspect);
225
+ return kept === undefined || kept >= STOCK_MIN_KEPT;
226
+ }
227
+
228
+ /** How well a photo answers a slot: its subject for the intent, its shape, the plan's words and the type's imagery. */
229
+ export function scoreStockPhoto(photo: StockPhotoTraits, need: StockNeed, imagery: ReadonlyArray<string> = []): number {
230
+ const subject = stockPhotoSubject(photo);
231
+ const rank = subject ? STOCK_INTENT_SUBJECTS[need.intent as StockIntent].indexOf(subject) : -1;
232
+ const subjectPoints = subject ? SUBJECT_POINTS[rank] ?? 0 : UNKNOWN_SUBJECT_POINTS;
233
+ const shapePoints = SHAPE_POINTS * (stockKept(photo, need.aspect) ?? UNKNOWN_KEPT);
234
+ const words = new Set(stockPhotoWords(photo));
235
+ const matched = need.words.filter(w => words.has(w)).length;
236
+ const imageryMatched = imagery.filter(w => words.has(w)).length;
237
+ return subjectPoints + shapePoints + WORD_POINTS * Math.min(MAX_WORD_MATCHES, matched) + IMAGERY_POINTS * Math.min(MAX_IMAGERY_MATCHES, imageryMatched);
238
+ }