@wix/web5-core 1.63.36 → 1.63.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/image/composeSemantic.js +76 -12
- package/dist/cjs/image/composeSemantic.js.map +1 -1
- package/dist/esm/image/composeSemantic.js +76 -12
- package/dist/esm/image/composeSemantic.js.map +1 -1
- package/dist/types/image/composeSemantic.d.ts +31 -0
- package/dist/types/image/composeSemantic.d.ts.map +1 -1
- package/package.json +2 -2
|
@@ -22,9 +22,55 @@ exports.composeSemantic = composeSemantic;
|
|
|
22
22
|
* CDN build externalises core, so a core change could not be seen through
|
|
23
23
|
* `?clientBundleUrl=` until it had been published. 0223 always assigned it
|
|
24
24
|
* here, and this is that move.
|
|
25
|
+
*
|
|
26
|
+
* WHAT THIS PHRASE IS FOR, AND WHY IT IS SHORT (ADR 0238)
|
|
27
|
+
* ------------------------------------------------------
|
|
28
|
+
* It buys **recall, and only recall**. In media-manager an editorial slot
|
|
29
|
+
* retrieves `SEMANTIC` over `semantic_text` capped at 30 candidates
|
|
30
|
+
* (`CandidateGather.planFor`), scoped by a filter that is just "this site's
|
|
31
|
+
* images" (`buildFilter`) — so the phrase ranks the corpus rather than gating
|
|
32
|
+
* it. Which picture then WINS is decided by `SlotScorer`, whose every term is
|
|
33
|
+
* geometric (crop fit, background, subject, text safety, resolution). Vespa's
|
|
34
|
+
* relevance score is not one of them, and the pool's order is discarded by the
|
|
35
|
+
* assignment solve. The phrase's entire job is choosing which 30 images reach
|
|
36
|
+
* the scorer.
|
|
37
|
+
*
|
|
38
|
+
* That is why it is budgeted in WORDS. The documents it is matched against are
|
|
39
|
+
* short — `ImageDocumentWriter.semanticText` embeds `alt, caption, label,
|
|
40
|
+
* label…`, usually under ten words — and retrieval is hybrid, BM25 blended with
|
|
41
|
+
* vector closeness. A headline plus a clause of marketing prose is the wrong
|
|
42
|
+
* shape against that on both halves: the vector drifts toward the mean of
|
|
43
|
+
* several topics instead of sitting on the subject, and BM25 spends term weight
|
|
44
|
+
* on filler that pulls in whatever else on the site happens to carry those
|
|
45
|
+
* words. Nothing normalises the query — case folding and dedupe (`LabelMerge`)
|
|
46
|
+
* happen on the document side only.
|
|
47
|
+
*
|
|
48
|
+
* A phrase that is too specific costs a worse 30, never a blank section: the
|
|
49
|
+
* site-wide widening in `CandidateGather.poolFor` is entity-only because an
|
|
50
|
+
* editorial pool empties only when the site has no images at all.
|
|
51
|
+
*
|
|
52
|
+
* CHANGING ANY OF THIS INVALIDATES SLOT IDS. `useImageSlot` derives a text
|
|
53
|
+
* slot's id as `s:${semantic}` (ADR 0223), so the phrase IS the cache key —
|
|
54
|
+
* every text-derived slot in the product gets a new id. Not a place for
|
|
55
|
+
* cosmetic edits.
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The phrase's real budget, in whole words — the unit that matters, because
|
|
60
|
+
* what it is matched against is a short bag of nouns and not a paragraph.
|
|
61
|
+
*/
|
|
62
|
+
const WORD_BUDGET = 10;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A title of this many words already names its own subject, so the lead adds
|
|
66
|
+
* dilution rather than signal and is dropped. Below it the title may be as thin
|
|
67
|
+
* as "Our picks" or "Why it matters", which names nothing retrievable on its
|
|
68
|
+
* own — that is the case the lead exists for.
|
|
25
69
|
*/
|
|
70
|
+
const TITLE_ENOUGH = 5;
|
|
26
71
|
|
|
27
|
-
/**
|
|
72
|
+
/** A character backstop under the word budget: ten words is normally well
|
|
73
|
+
* inside it, but one pathological unbroken "word" is not. */
|
|
28
74
|
const MAX_LENGTH = 120;
|
|
29
75
|
|
|
30
76
|
/** Below this there is no query worth sending — better to render no picture
|
|
@@ -39,9 +85,18 @@ const stripMarkdown = raw => raw
|
|
|
39
85
|
.replace(/^\s*#{1,6}\s*/gm, '').replace(/^\s*>\s?/gm, '')
|
|
40
86
|
// html tags an author or the parser may have left behind
|
|
41
87
|
.replace(/<[^>]+>/g, ' ');
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
.
|
|
88
|
+
const trimTail = raw => raw.replace(/[\s.,;:!?—–-]+$/g, '').trim();
|
|
89
|
+
const tidy = raw => trimTail(stripMarkdown(raw).replace(/\s+/g, ' ').trim());
|
|
90
|
+
const wordsOf = raw => raw.split(' ').filter(word => word.length > 0);
|
|
91
|
+
|
|
92
|
+
/** Keep at most `max` whole words. The unit the budget is actually stated in. */
|
|
93
|
+
const clampToWords = (raw, max) => {
|
|
94
|
+
const words = wordsOf(raw);
|
|
95
|
+
if (words.length <= max) {
|
|
96
|
+
return raw;
|
|
97
|
+
}
|
|
98
|
+
return trimTail(words.slice(0, max).join(' '));
|
|
99
|
+
};
|
|
45
100
|
|
|
46
101
|
/**
|
|
47
102
|
* Clamp without cutting a word in half. Slicing at a fixed length leaves
|
|
@@ -58,9 +113,12 @@ const clampToWord = (raw, max) => {
|
|
|
58
113
|
const cut = raw.slice(0, max + 1);
|
|
59
114
|
const lastBreak = cut.lastIndexOf(' ');
|
|
60
115
|
const out = lastBreak > 0 ? cut.slice(0, lastBreak) : raw.slice(0, max);
|
|
61
|
-
return out
|
|
116
|
+
return trimTail(out);
|
|
62
117
|
};
|
|
63
118
|
|
|
119
|
+
/** The word budget first, then the character backstop under it. */
|
|
120
|
+
const budgeted = raw => clampToWord(clampToWords(raw, WORD_BUDGET), MAX_LENGTH);
|
|
121
|
+
|
|
64
122
|
/** The first clause carries the subject; what follows is usually qualification. */
|
|
65
123
|
const firstClause = raw => {
|
|
66
124
|
const m = raw.match(/^[^.!?;]+/);
|
|
@@ -78,17 +136,23 @@ function composeSemantic({
|
|
|
78
136
|
// A mission, when there is one, is the whole answer.
|
|
79
137
|
const mission = tidy(sectionSemantic ?? '');
|
|
80
138
|
if (mission.length >= MIN_LENGTH) {
|
|
81
|
-
return
|
|
139
|
+
return budgeted(mission);
|
|
82
140
|
}
|
|
83
141
|
const head = tidy(title ?? '');
|
|
84
|
-
const
|
|
142
|
+
const headWords = wordsOf(head).length;
|
|
143
|
+
|
|
144
|
+
// The title is the stronger signal, so it is spent first and never truncated
|
|
145
|
+
// to make room for the lead. Once it names a subject on its own, it IS the
|
|
146
|
+
// phrase.
|
|
147
|
+
if (headWords >= TITLE_ENOUGH) {
|
|
148
|
+
const out = budgeted(head);
|
|
149
|
+
return out.length < MIN_LENGTH ? '' : out;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// A thin title spends what it did not use on the lead's first clause.
|
|
153
|
+
const body = clampToWords(firstClause(tidy(lead ?? '')), WORD_BUDGET - headWords);
|
|
85
154
|
const joined = [head, body].filter(Boolean).join('. ');
|
|
86
155
|
if (joined.length < MIN_LENGTH) return '';
|
|
87
|
-
if (joined.length <= MAX_LENGTH) return joined;
|
|
88
|
-
|
|
89
|
-
// Over budget: keep the title whole if it fits, since it is the stronger
|
|
90
|
-
// signal, and spend whatever is left on the lead.
|
|
91
|
-
if (head.length >= MAX_LENGTH) return clampToWord(head, MAX_LENGTH);
|
|
92
156
|
return clampToWord(joined, MAX_LENGTH);
|
|
93
157
|
}
|
|
94
158
|
//# sourceMappingURL=composeSemantic.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["MAX_LENGTH","MIN_LENGTH","stripMarkdown","raw","replace","tidy","trim","clampToWord","max","length","cut","slice","lastBreak","lastIndexOf","out","firstClause","m","match","composeSemantic","title","lead","sectionSemantic","mission","head","body","joined","filter","Boolean","join"],"sources":["../../../src/image/composeSemantic.ts"],"sourcesContent":["/**\n * Turn a section's own words into a retrieval phrase.\n *\n * The section hands over its RAW props — a title, a lead paragraph — not a\n * query string. Composing is this module's job, in one place, because the two\n * call sites that do it today invent their own phrase, do not agree with each\n * other, and neither is tested:\n *\n * w5-client-circana/src/components/sections/GenericEntitySection.tsx:144\n * `web5://image/${title}` — raw, so `**Nike** collections`\n * is searched WITH its asterisks\n * w5-client-circana/src/components/sections/ResearchSection.tsx:68\n * `web5://image/${encodeURIComponent(title)}` — same idea, escaped\n *\n * Centralising it is most of what the text-derived case is worth.\n *\n * Lifted from the seed client package (ADR 0225): it started there because the\n * CDN build externalises core, so a core change could not be seen through\n * `?clientBundleUrl=` until it had been published. 0223 always assigned it\n * here, and this is that move.\n */\n\n/** Longer than this and the tail stops helping retrieval and starts diluting it. */\nconst MAX_LENGTH = 120;\n\n/** Below this there is no query worth sending — better to render no picture\n * than to retrieve on a word like \"More\". */\nconst MIN_LENGTH = 3;\n\nconst stripMarkdown = (raw: string): string =>\n raw\n // images before links:  would otherwise leave a stray `!`\n .replace(/!\\[([^\\]]*)\\]\\([^)]*\\)/g, '$1')\n .replace(/\\[([^\\]]*)\\]\\([^)]*\\)/g, '$1')\n // emphasis / strong / strike / inline code — keep the words, drop the marks\n .replace(/(\\*\\*\\*|\\*\\*|\\*|___|__|_|~~|`)/g, '')\n // leading heading hashes and blockquote marks\n .replace(/^\\s*#{1,6}\\s*/gm, '')\n .replace(/^\\s*>\\s?/gm, '')\n // html tags an author or the parser may have left behind\n .replace(/<[^>]+>/g, ' ');\n\nconst tidy = (raw: string): string =>\n stripMarkdown(raw)\n .replace(/\\s+/g, ' ')\n .trim()\n // trailing punctuation is noise in a retrieval phrase\n .replace(/[\\s.,;:!?—–-]+$/g, '')\n .trim();\n\n/**\n * Clamp without cutting a word in half. Slicing at a fixed length leaves\n * fragments like \"the kinds of product\" — observed in the first real run — and\n * a dangling partial word is noise in a retrieval phrase, not a shorter\n * version of it. Falls back to a hard slice only when the budget cannot fit\n * even one word.\n */\nconst clampToWord = (raw: string, max: number): string => {\n if (raw.length <= max) {\n return raw;\n }\n // +1 so a boundary landing exactly on the budget still counts as a break.\n const cut = raw.slice(0, max + 1);\n const lastBreak = cut.lastIndexOf(' ');\n const out = lastBreak > 0 ? cut.slice(0, lastBreak) : raw.slice(0, max);\n return out.replace(/[\\s.,;:!?—–-]+$/g, '').trim();\n};\n\n/** The first clause carries the subject; what follows is usually qualification. */\nconst firstClause = (raw: string): string => {\n const m = raw.match(/^[^.!?;]+/);\n return (m ? m[0] : raw).trim();\n};\n\nexport interface ComposeSemanticInput {\n /** The section's headline. */\n title?: string;\n /** Its lead paragraph or body copy. */\n lead?: string;\n /** The descriptor's own `semantic` / mission, when the host supplies one.\n * Outranks the prose: it is what the orchestrator meant the section to be\n * about, which is a better subject than what it happened to say. */\n sectionSemantic?: string;\n}\n\n/**\n * Returns the phrase, or `''` when what survives is too thin to retrieve on —\n * the caller then declares no slot at all rather than sending a bad query.\n */\nexport function composeSemantic({\n title,\n lead,\n sectionSemantic,\n}: ComposeSemanticInput): string {\n // A mission, when there is one, is the whole answer.\n const mission = tidy(sectionSemantic ?? '');\n if (mission.length >= MIN_LENGTH) {\n return clampToWord(mission, MAX_LENGTH);\n }\n\n const head = tidy(title ?? '');\n const body = firstClause(tidy(lead ?? ''));\n\n const joined = [head, body].filter(Boolean).join('. ');\n if (joined.length < MIN_LENGTH) return '';\n\n if (joined.length <= MAX_LENGTH) return joined;\n\n // Over budget: keep the title whole if it fits, since it is the stronger\n // signal, and spend whatever is left on the lead.\n if (head.length >= MAX_LENGTH) return clampToWord(head, MAX_LENGTH);\n return clampToWord(joined, MAX_LENGTH);\n}\n"],"mappings":"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","ignoreList":[]}
|
|
1
|
+
{"version":3,"names":["WORD_BUDGET","TITLE_ENOUGH","MAX_LENGTH","MIN_LENGTH","stripMarkdown","raw","replace","trimTail","trim","tidy","wordsOf","split","filter","word","length","clampToWords","max","words","slice","join","clampToWord","cut","lastBreak","lastIndexOf","out","budgeted","firstClause","m","match","composeSemantic","title","lead","sectionSemantic","mission","head","headWords","body","joined","Boolean"],"sources":["../../../src/image/composeSemantic.ts"],"sourcesContent":["/**\n * Turn a section's own words into a retrieval phrase.\n *\n * The section hands over its RAW props — a title, a lead paragraph — not a\n * query string. Composing is this module's job, in one place, because the two\n * call sites that do it today invent their own phrase, do not agree with each\n * other, and neither is tested:\n *\n * w5-client-circana/src/components/sections/GenericEntitySection.tsx:144\n * `web5://image/${title}` — raw, so `**Nike** collections`\n * is searched WITH its asterisks\n * w5-client-circana/src/components/sections/ResearchSection.tsx:68\n * `web5://image/${encodeURIComponent(title)}` — same idea, escaped\n *\n * Centralising it is most of what the text-derived case is worth.\n *\n * Lifted from the seed client package (ADR 0225): it started there because the\n * CDN build externalises core, so a core change could not be seen through\n * `?clientBundleUrl=` until it had been published. 0223 always assigned it\n * here, and this is that move.\n *\n * WHAT THIS PHRASE IS FOR, AND WHY IT IS SHORT (ADR 0238)\n * ------------------------------------------------------\n * It buys **recall, and only recall**. In media-manager an editorial slot\n * retrieves `SEMANTIC` over `semantic_text` capped at 30 candidates\n * (`CandidateGather.planFor`), scoped by a filter that is just \"this site's\n * images\" (`buildFilter`) — so the phrase ranks the corpus rather than gating\n * it. Which picture then WINS is decided by `SlotScorer`, whose every term is\n * geometric (crop fit, background, subject, text safety, resolution). Vespa's\n * relevance score is not one of them, and the pool's order is discarded by the\n * assignment solve. The phrase's entire job is choosing which 30 images reach\n * the scorer.\n *\n * That is why it is budgeted in WORDS. The documents it is matched against are\n * short — `ImageDocumentWriter.semanticText` embeds `alt, caption, label,\n * label…`, usually under ten words — and retrieval is hybrid, BM25 blended with\n * vector closeness. A headline plus a clause of marketing prose is the wrong\n * shape against that on both halves: the vector drifts toward the mean of\n * several topics instead of sitting on the subject, and BM25 spends term weight\n * on filler that pulls in whatever else on the site happens to carry those\n * words. Nothing normalises the query — case folding and dedupe (`LabelMerge`)\n * happen on the document side only.\n *\n * A phrase that is too specific costs a worse 30, never a blank section: the\n * site-wide widening in `CandidateGather.poolFor` is entity-only because an\n * editorial pool empties only when the site has no images at all.\n *\n * CHANGING ANY OF THIS INVALIDATES SLOT IDS. `useImageSlot` derives a text\n * slot's id as `s:${semantic}` (ADR 0223), so the phrase IS the cache key —\n * every text-derived slot in the product gets a new id. Not a place for\n * cosmetic edits.\n */\n\n/**\n * The phrase's real budget, in whole words — the unit that matters, because\n * what it is matched against is a short bag of nouns and not a paragraph.\n */\nconst WORD_BUDGET = 10;\n\n/**\n * A title of this many words already names its own subject, so the lead adds\n * dilution rather than signal and is dropped. Below it the title may be as thin\n * as \"Our picks\" or \"Why it matters\", which names nothing retrievable on its\n * own — that is the case the lead exists for.\n */\nconst TITLE_ENOUGH = 5;\n\n/** A character backstop under the word budget: ten words is normally well\n * inside it, but one pathological unbroken \"word\" is not. */\nconst MAX_LENGTH = 120;\n\n/** Below this there is no query worth sending — better to render no picture\n * than to retrieve on a word like \"More\". */\nconst MIN_LENGTH = 3;\n\nconst stripMarkdown = (raw: string): string =>\n raw\n // images before links:  would otherwise leave a stray `!`\n .replace(/!\\[([^\\]]*)\\]\\([^)]*\\)/g, '$1')\n .replace(/\\[([^\\]]*)\\]\\([^)]*\\)/g, '$1')\n // emphasis / strong / strike / inline code — keep the words, drop the marks\n .replace(/(\\*\\*\\*|\\*\\*|\\*|___|__|_|~~|`)/g, '')\n // leading heading hashes and blockquote marks\n .replace(/^\\s*#{1,6}\\s*/gm, '')\n .replace(/^\\s*>\\s?/gm, '')\n // html tags an author or the parser may have left behind\n .replace(/<[^>]+>/g, ' ');\n\nconst trimTail = (raw: string): string =>\n raw.replace(/[\\s.,;:!?—–-]+$/g, '').trim();\n\nconst tidy = (raw: string): string =>\n trimTail(stripMarkdown(raw).replace(/\\s+/g, ' ').trim());\n\nconst wordsOf = (raw: string): string[] =>\n raw.split(' ').filter((word) => word.length > 0);\n\n/** Keep at most `max` whole words. The unit the budget is actually stated in. */\nconst clampToWords = (raw: string, max: number): string => {\n const words = wordsOf(raw);\n if (words.length <= max) {\n return raw;\n }\n return trimTail(words.slice(0, max).join(' '));\n};\n\n/**\n * Clamp without cutting a word in half. Slicing at a fixed length leaves\n * fragments like \"the kinds of product\" — observed in the first real run — and\n * a dangling partial word is noise in a retrieval phrase, not a shorter\n * version of it. Falls back to a hard slice only when the budget cannot fit\n * even one word.\n */\nconst clampToWord = (raw: string, max: number): string => {\n if (raw.length <= max) {\n return raw;\n }\n // +1 so a boundary landing exactly on the budget still counts as a break.\n const cut = raw.slice(0, max + 1);\n const lastBreak = cut.lastIndexOf(' ');\n const out = lastBreak > 0 ? cut.slice(0, lastBreak) : raw.slice(0, max);\n return trimTail(out);\n};\n\n/** The word budget first, then the character backstop under it. */\nconst budgeted = (raw: string): string =>\n clampToWord(clampToWords(raw, WORD_BUDGET), MAX_LENGTH);\n\n/** The first clause carries the subject; what follows is usually qualification. */\nconst firstClause = (raw: string): string => {\n const m = raw.match(/^[^.!?;]+/);\n return (m ? m[0] : raw).trim();\n};\n\nexport interface ComposeSemanticInput {\n /** The section's headline. */\n title?: string;\n /** Its lead paragraph or body copy. */\n lead?: string;\n /** The descriptor's own `semantic` / mission, when the host supplies one.\n * Outranks the prose: it is what the orchestrator meant the section to be\n * about, which is a better subject than what it happened to say. */\n sectionSemantic?: string;\n}\n\n/**\n * Returns the phrase, or `''` when what survives is too thin to retrieve on —\n * the caller then declares no slot at all rather than sending a bad query.\n */\nexport function composeSemantic({\n title,\n lead,\n sectionSemantic,\n}: ComposeSemanticInput): string {\n // A mission, when there is one, is the whole answer.\n const mission = tidy(sectionSemantic ?? '');\n if (mission.length >= MIN_LENGTH) {\n return budgeted(mission);\n }\n\n const head = tidy(title ?? '');\n const headWords = wordsOf(head).length;\n\n // The title is the stronger signal, so it is spent first and never truncated\n // to make room for the lead. Once it names a subject on its own, it IS the\n // phrase.\n if (headWords >= TITLE_ENOUGH) {\n const out = budgeted(head);\n return out.length < MIN_LENGTH ? '' : out;\n }\n\n // A thin title spends what it did not use on the lead's first clause.\n const body = clampToWords(\n firstClause(tidy(lead ?? '')),\n WORD_BUDGET - headWords,\n );\n\n const joined = [head, body].filter(Boolean).join('. ');\n if (joined.length < MIN_LENGTH) return '';\n\n return clampToWord(joined, MAX_LENGTH);\n}\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -18,9 +18,55 @@
|
|
|
18
18
|
* CDN build externalises core, so a core change could not be seen through
|
|
19
19
|
* `?clientBundleUrl=` until it had been published. 0223 always assigned it
|
|
20
20
|
* here, and this is that move.
|
|
21
|
+
*
|
|
22
|
+
* WHAT THIS PHRASE IS FOR, AND WHY IT IS SHORT (ADR 0238)
|
|
23
|
+
* ------------------------------------------------------
|
|
24
|
+
* It buys **recall, and only recall**. In media-manager an editorial slot
|
|
25
|
+
* retrieves `SEMANTIC` over `semantic_text` capped at 30 candidates
|
|
26
|
+
* (`CandidateGather.planFor`), scoped by a filter that is just "this site's
|
|
27
|
+
* images" (`buildFilter`) — so the phrase ranks the corpus rather than gating
|
|
28
|
+
* it. Which picture then WINS is decided by `SlotScorer`, whose every term is
|
|
29
|
+
* geometric (crop fit, background, subject, text safety, resolution). Vespa's
|
|
30
|
+
* relevance score is not one of them, and the pool's order is discarded by the
|
|
31
|
+
* assignment solve. The phrase's entire job is choosing which 30 images reach
|
|
32
|
+
* the scorer.
|
|
33
|
+
*
|
|
34
|
+
* That is why it is budgeted in WORDS. The documents it is matched against are
|
|
35
|
+
* short — `ImageDocumentWriter.semanticText` embeds `alt, caption, label,
|
|
36
|
+
* label…`, usually under ten words — and retrieval is hybrid, BM25 blended with
|
|
37
|
+
* vector closeness. A headline plus a clause of marketing prose is the wrong
|
|
38
|
+
* shape against that on both halves: the vector drifts toward the mean of
|
|
39
|
+
* several topics instead of sitting on the subject, and BM25 spends term weight
|
|
40
|
+
* on filler that pulls in whatever else on the site happens to carry those
|
|
41
|
+
* words. Nothing normalises the query — case folding and dedupe (`LabelMerge`)
|
|
42
|
+
* happen on the document side only.
|
|
43
|
+
*
|
|
44
|
+
* A phrase that is too specific costs a worse 30, never a blank section: the
|
|
45
|
+
* site-wide widening in `CandidateGather.poolFor` is entity-only because an
|
|
46
|
+
* editorial pool empties only when the site has no images at all.
|
|
47
|
+
*
|
|
48
|
+
* CHANGING ANY OF THIS INVALIDATES SLOT IDS. `useImageSlot` derives a text
|
|
49
|
+
* slot's id as `s:${semantic}` (ADR 0223), so the phrase IS the cache key —
|
|
50
|
+
* every text-derived slot in the product gets a new id. Not a place for
|
|
51
|
+
* cosmetic edits.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The phrase's real budget, in whole words — the unit that matters, because
|
|
56
|
+
* what it is matched against is a short bag of nouns and not a paragraph.
|
|
57
|
+
*/
|
|
58
|
+
const WORD_BUDGET = 10;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* A title of this many words already names its own subject, so the lead adds
|
|
62
|
+
* dilution rather than signal and is dropped. Below it the title may be as thin
|
|
63
|
+
* as "Our picks" or "Why it matters", which names nothing retrievable on its
|
|
64
|
+
* own — that is the case the lead exists for.
|
|
21
65
|
*/
|
|
66
|
+
const TITLE_ENOUGH = 5;
|
|
22
67
|
|
|
23
|
-
/**
|
|
68
|
+
/** A character backstop under the word budget: ten words is normally well
|
|
69
|
+
* inside it, but one pathological unbroken "word" is not. */
|
|
24
70
|
const MAX_LENGTH = 120;
|
|
25
71
|
|
|
26
72
|
/** Below this there is no query worth sending — better to render no picture
|
|
@@ -35,9 +81,18 @@ const stripMarkdown = raw => raw
|
|
|
35
81
|
.replace(/^\s*#{1,6}\s*/gm, '').replace(/^\s*>\s?/gm, '')
|
|
36
82
|
// html tags an author or the parser may have left behind
|
|
37
83
|
.replace(/<[^>]+>/g, ' ');
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
.
|
|
84
|
+
const trimTail = raw => raw.replace(/[\s.,;:!?—–-]+$/g, '').trim();
|
|
85
|
+
const tidy = raw => trimTail(stripMarkdown(raw).replace(/\s+/g, ' ').trim());
|
|
86
|
+
const wordsOf = raw => raw.split(' ').filter(word => word.length > 0);
|
|
87
|
+
|
|
88
|
+
/** Keep at most `max` whole words. The unit the budget is actually stated in. */
|
|
89
|
+
const clampToWords = (raw, max) => {
|
|
90
|
+
const words = wordsOf(raw);
|
|
91
|
+
if (words.length <= max) {
|
|
92
|
+
return raw;
|
|
93
|
+
}
|
|
94
|
+
return trimTail(words.slice(0, max).join(' '));
|
|
95
|
+
};
|
|
41
96
|
|
|
42
97
|
/**
|
|
43
98
|
* Clamp without cutting a word in half. Slicing at a fixed length leaves
|
|
@@ -54,9 +109,12 @@ const clampToWord = (raw, max) => {
|
|
|
54
109
|
const cut = raw.slice(0, max + 1);
|
|
55
110
|
const lastBreak = cut.lastIndexOf(' ');
|
|
56
111
|
const out = lastBreak > 0 ? cut.slice(0, lastBreak) : raw.slice(0, max);
|
|
57
|
-
return out
|
|
112
|
+
return trimTail(out);
|
|
58
113
|
};
|
|
59
114
|
|
|
115
|
+
/** The word budget first, then the character backstop under it. */
|
|
116
|
+
const budgeted = raw => clampToWord(clampToWords(raw, WORD_BUDGET), MAX_LENGTH);
|
|
117
|
+
|
|
60
118
|
/** The first clause carries the subject; what follows is usually qualification. */
|
|
61
119
|
const firstClause = raw => {
|
|
62
120
|
const m = raw.match(/^[^.!?;]+/);
|
|
@@ -75,17 +133,23 @@ export function composeSemantic(_ref) {
|
|
|
75
133
|
// A mission, when there is one, is the whole answer.
|
|
76
134
|
const mission = tidy(sectionSemantic ?? '');
|
|
77
135
|
if (mission.length >= MIN_LENGTH) {
|
|
78
|
-
return
|
|
136
|
+
return budgeted(mission);
|
|
79
137
|
}
|
|
80
138
|
const head = tidy(title ?? '');
|
|
81
|
-
const
|
|
139
|
+
const headWords = wordsOf(head).length;
|
|
140
|
+
|
|
141
|
+
// The title is the stronger signal, so it is spent first and never truncated
|
|
142
|
+
// to make room for the lead. Once it names a subject on its own, it IS the
|
|
143
|
+
// phrase.
|
|
144
|
+
if (headWords >= TITLE_ENOUGH) {
|
|
145
|
+
const out = budgeted(head);
|
|
146
|
+
return out.length < MIN_LENGTH ? '' : out;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// A thin title spends what it did not use on the lead's first clause.
|
|
150
|
+
const body = clampToWords(firstClause(tidy(lead ?? '')), WORD_BUDGET - headWords);
|
|
82
151
|
const joined = [head, body].filter(Boolean).join('. ');
|
|
83
152
|
if (joined.length < MIN_LENGTH) return '';
|
|
84
|
-
if (joined.length <= MAX_LENGTH) return joined;
|
|
85
|
-
|
|
86
|
-
// Over budget: keep the title whole if it fits, since it is the stronger
|
|
87
|
-
// signal, and spend whatever is left on the lead.
|
|
88
|
-
if (head.length >= MAX_LENGTH) return clampToWord(head, MAX_LENGTH);
|
|
89
153
|
return clampToWord(joined, MAX_LENGTH);
|
|
90
154
|
}
|
|
91
155
|
//# sourceMappingURL=composeSemantic.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["MAX_LENGTH","MIN_LENGTH","stripMarkdown","raw","replace","tidy","trim","clampToWord","max","length","cut","slice","lastBreak","lastIndexOf","out","firstClause","m","match","composeSemantic","_ref","title","lead","sectionSemantic","mission","head","body","joined","filter","Boolean","join"],"sources":["../../../src/image/composeSemantic.ts"],"sourcesContent":["/**\n * Turn a section's own words into a retrieval phrase.\n *\n * The section hands over its RAW props — a title, a lead paragraph — not a\n * query string. Composing is this module's job, in one place, because the two\n * call sites that do it today invent their own phrase, do not agree with each\n * other, and neither is tested:\n *\n * w5-client-circana/src/components/sections/GenericEntitySection.tsx:144\n * `web5://image/${title}` — raw, so `**Nike** collections`\n * is searched WITH its asterisks\n * w5-client-circana/src/components/sections/ResearchSection.tsx:68\n * `web5://image/${encodeURIComponent(title)}` — same idea, escaped\n *\n * Centralising it is most of what the text-derived case is worth.\n *\n * Lifted from the seed client package (ADR 0225): it started there because the\n * CDN build externalises core, so a core change could not be seen through\n * `?clientBundleUrl=` until it had been published. 0223 always assigned it\n * here, and this is that move.\n */\n\n/** Longer than this and the tail stops helping retrieval and starts diluting it. */\nconst MAX_LENGTH = 120;\n\n/** Below this there is no query worth sending — better to render no picture\n * than to retrieve on a word like \"More\". */\nconst MIN_LENGTH = 3;\n\nconst stripMarkdown = (raw: string): string =>\n raw\n // images before links:  would otherwise leave a stray `!`\n .replace(/!\\[([^\\]]*)\\]\\([^)]*\\)/g, '$1')\n .replace(/\\[([^\\]]*)\\]\\([^)]*\\)/g, '$1')\n // emphasis / strong / strike / inline code — keep the words, drop the marks\n .replace(/(\\*\\*\\*|\\*\\*|\\*|___|__|_|~~|`)/g, '')\n // leading heading hashes and blockquote marks\n .replace(/^\\s*#{1,6}\\s*/gm, '')\n .replace(/^\\s*>\\s?/gm, '')\n // html tags an author or the parser may have left behind\n .replace(/<[^>]+>/g, ' ');\n\nconst tidy = (raw: string): string =>\n stripMarkdown(raw)\n .replace(/\\s+/g, ' ')\n .trim()\n // trailing punctuation is noise in a retrieval phrase\n .replace(/[\\s.,;:!?—–-]+$/g, '')\n .trim();\n\n/**\n * Clamp without cutting a word in half. Slicing at a fixed length leaves\n * fragments like \"the kinds of product\" — observed in the first real run — and\n * a dangling partial word is noise in a retrieval phrase, not a shorter\n * version of it. Falls back to a hard slice only when the budget cannot fit\n * even one word.\n */\nconst clampToWord = (raw: string, max: number): string => {\n if (raw.length <= max) {\n return raw;\n }\n // +1 so a boundary landing exactly on the budget still counts as a break.\n const cut = raw.slice(0, max + 1);\n const lastBreak = cut.lastIndexOf(' ');\n const out = lastBreak > 0 ? cut.slice(0, lastBreak) : raw.slice(0, max);\n return out.replace(/[\\s.,;:!?—–-]+$/g, '').trim();\n};\n\n/** The first clause carries the subject; what follows is usually qualification. */\nconst firstClause = (raw: string): string => {\n const m = raw.match(/^[^.!?;]+/);\n return (m ? m[0] : raw).trim();\n};\n\nexport interface ComposeSemanticInput {\n /** The section's headline. */\n title?: string;\n /** Its lead paragraph or body copy. */\n lead?: string;\n /** The descriptor's own `semantic` / mission, when the host supplies one.\n * Outranks the prose: it is what the orchestrator meant the section to be\n * about, which is a better subject than what it happened to say. */\n sectionSemantic?: string;\n}\n\n/**\n * Returns the phrase, or `''` when what survives is too thin to retrieve on —\n * the caller then declares no slot at all rather than sending a bad query.\n */\nexport function composeSemantic({\n title,\n lead,\n sectionSemantic,\n}: ComposeSemanticInput): string {\n // A mission, when there is one, is the whole answer.\n const mission = tidy(sectionSemantic ?? '');\n if (mission.length >= MIN_LENGTH) {\n return clampToWord(mission, MAX_LENGTH);\n }\n\n const head = tidy(title ?? '');\n const body = firstClause(tidy(lead ?? ''));\n\n const joined = [head, body].filter(Boolean).join('. ');\n if (joined.length < MIN_LENGTH) return '';\n\n if (joined.length <= MAX_LENGTH) return joined;\n\n // Over budget: keep the title whole if it fits, since it is the stronger\n // signal, and spend whatever is left on the lead.\n if (head.length >= MAX_LENGTH) return clampToWord(head, MAX_LENGTH);\n return clampToWord(joined, MAX_LENGTH);\n}\n"],"mappings":"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","ignoreList":[]}
|
|
1
|
+
{"version":3,"names":["WORD_BUDGET","TITLE_ENOUGH","MAX_LENGTH","MIN_LENGTH","stripMarkdown","raw","replace","trimTail","trim","tidy","wordsOf","split","filter","word","length","clampToWords","max","words","slice","join","clampToWord","cut","lastBreak","lastIndexOf","out","budgeted","firstClause","m","match","composeSemantic","_ref","title","lead","sectionSemantic","mission","head","headWords","body","joined","Boolean"],"sources":["../../../src/image/composeSemantic.ts"],"sourcesContent":["/**\n * Turn a section's own words into a retrieval phrase.\n *\n * The section hands over its RAW props — a title, a lead paragraph — not a\n * query string. Composing is this module's job, in one place, because the two\n * call sites that do it today invent their own phrase, do not agree with each\n * other, and neither is tested:\n *\n * w5-client-circana/src/components/sections/GenericEntitySection.tsx:144\n * `web5://image/${title}` — raw, so `**Nike** collections`\n * is searched WITH its asterisks\n * w5-client-circana/src/components/sections/ResearchSection.tsx:68\n * `web5://image/${encodeURIComponent(title)}` — same idea, escaped\n *\n * Centralising it is most of what the text-derived case is worth.\n *\n * Lifted from the seed client package (ADR 0225): it started there because the\n * CDN build externalises core, so a core change could not be seen through\n * `?clientBundleUrl=` until it had been published. 0223 always assigned it\n * here, and this is that move.\n *\n * WHAT THIS PHRASE IS FOR, AND WHY IT IS SHORT (ADR 0238)\n * ------------------------------------------------------\n * It buys **recall, and only recall**. In media-manager an editorial slot\n * retrieves `SEMANTIC` over `semantic_text` capped at 30 candidates\n * (`CandidateGather.planFor`), scoped by a filter that is just \"this site's\n * images\" (`buildFilter`) — so the phrase ranks the corpus rather than gating\n * it. Which picture then WINS is decided by `SlotScorer`, whose every term is\n * geometric (crop fit, background, subject, text safety, resolution). Vespa's\n * relevance score is not one of them, and the pool's order is discarded by the\n * assignment solve. The phrase's entire job is choosing which 30 images reach\n * the scorer.\n *\n * That is why it is budgeted in WORDS. The documents it is matched against are\n * short — `ImageDocumentWriter.semanticText` embeds `alt, caption, label,\n * label…`, usually under ten words — and retrieval is hybrid, BM25 blended with\n * vector closeness. A headline plus a clause of marketing prose is the wrong\n * shape against that on both halves: the vector drifts toward the mean of\n * several topics instead of sitting on the subject, and BM25 spends term weight\n * on filler that pulls in whatever else on the site happens to carry those\n * words. Nothing normalises the query — case folding and dedupe (`LabelMerge`)\n * happen on the document side only.\n *\n * A phrase that is too specific costs a worse 30, never a blank section: the\n * site-wide widening in `CandidateGather.poolFor` is entity-only because an\n * editorial pool empties only when the site has no images at all.\n *\n * CHANGING ANY OF THIS INVALIDATES SLOT IDS. `useImageSlot` derives a text\n * slot's id as `s:${semantic}` (ADR 0223), so the phrase IS the cache key —\n * every text-derived slot in the product gets a new id. Not a place for\n * cosmetic edits.\n */\n\n/**\n * The phrase's real budget, in whole words — the unit that matters, because\n * what it is matched against is a short bag of nouns and not a paragraph.\n */\nconst WORD_BUDGET = 10;\n\n/**\n * A title of this many words already names its own subject, so the lead adds\n * dilution rather than signal and is dropped. Below it the title may be as thin\n * as \"Our picks\" or \"Why it matters\", which names nothing retrievable on its\n * own — that is the case the lead exists for.\n */\nconst TITLE_ENOUGH = 5;\n\n/** A character backstop under the word budget: ten words is normally well\n * inside it, but one pathological unbroken \"word\" is not. */\nconst MAX_LENGTH = 120;\n\n/** Below this there is no query worth sending — better to render no picture\n * than to retrieve on a word like \"More\". */\nconst MIN_LENGTH = 3;\n\nconst stripMarkdown = (raw: string): string =>\n raw\n // images before links:  would otherwise leave a stray `!`\n .replace(/!\\[([^\\]]*)\\]\\([^)]*\\)/g, '$1')\n .replace(/\\[([^\\]]*)\\]\\([^)]*\\)/g, '$1')\n // emphasis / strong / strike / inline code — keep the words, drop the marks\n .replace(/(\\*\\*\\*|\\*\\*|\\*|___|__|_|~~|`)/g, '')\n // leading heading hashes and blockquote marks\n .replace(/^\\s*#{1,6}\\s*/gm, '')\n .replace(/^\\s*>\\s?/gm, '')\n // html tags an author or the parser may have left behind\n .replace(/<[^>]+>/g, ' ');\n\nconst trimTail = (raw: string): string =>\n raw.replace(/[\\s.,;:!?—–-]+$/g, '').trim();\n\nconst tidy = (raw: string): string =>\n trimTail(stripMarkdown(raw).replace(/\\s+/g, ' ').trim());\n\nconst wordsOf = (raw: string): string[] =>\n raw.split(' ').filter((word) => word.length > 0);\n\n/** Keep at most `max` whole words. The unit the budget is actually stated in. */\nconst clampToWords = (raw: string, max: number): string => {\n const words = wordsOf(raw);\n if (words.length <= max) {\n return raw;\n }\n return trimTail(words.slice(0, max).join(' '));\n};\n\n/**\n * Clamp without cutting a word in half. Slicing at a fixed length leaves\n * fragments like \"the kinds of product\" — observed in the first real run — and\n * a dangling partial word is noise in a retrieval phrase, not a shorter\n * version of it. Falls back to a hard slice only when the budget cannot fit\n * even one word.\n */\nconst clampToWord = (raw: string, max: number): string => {\n if (raw.length <= max) {\n return raw;\n }\n // +1 so a boundary landing exactly on the budget still counts as a break.\n const cut = raw.slice(0, max + 1);\n const lastBreak = cut.lastIndexOf(' ');\n const out = lastBreak > 0 ? cut.slice(0, lastBreak) : raw.slice(0, max);\n return trimTail(out);\n};\n\n/** The word budget first, then the character backstop under it. */\nconst budgeted = (raw: string): string =>\n clampToWord(clampToWords(raw, WORD_BUDGET), MAX_LENGTH);\n\n/** The first clause carries the subject; what follows is usually qualification. */\nconst firstClause = (raw: string): string => {\n const m = raw.match(/^[^.!?;]+/);\n return (m ? m[0] : raw).trim();\n};\n\nexport interface ComposeSemanticInput {\n /** The section's headline. */\n title?: string;\n /** Its lead paragraph or body copy. */\n lead?: string;\n /** The descriptor's own `semantic` / mission, when the host supplies one.\n * Outranks the prose: it is what the orchestrator meant the section to be\n * about, which is a better subject than what it happened to say. */\n sectionSemantic?: string;\n}\n\n/**\n * Returns the phrase, or `''` when what survives is too thin to retrieve on —\n * the caller then declares no slot at all rather than sending a bad query.\n */\nexport function composeSemantic({\n title,\n lead,\n sectionSemantic,\n}: ComposeSemanticInput): string {\n // A mission, when there is one, is the whole answer.\n const mission = tidy(sectionSemantic ?? '');\n if (mission.length >= MIN_LENGTH) {\n return budgeted(mission);\n }\n\n const head = tidy(title ?? '');\n const headWords = wordsOf(head).length;\n\n // The title is the stronger signal, so it is spent first and never truncated\n // to make room for the lead. Once it names a subject on its own, it IS the\n // phrase.\n if (headWords >= TITLE_ENOUGH) {\n const out = budgeted(head);\n return out.length < MIN_LENGTH ? '' : out;\n }\n\n // A thin title spends what it did not use on the lead's first clause.\n const body = clampToWords(\n firstClause(tidy(lead ?? '')),\n WORD_BUDGET - headWords,\n );\n\n const joined = [head, body].filter(Boolean).join('. ');\n if (joined.length < MIN_LENGTH) return '';\n\n return clampToWord(joined, MAX_LENGTH);\n}\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -18,6 +18,37 @@
|
|
|
18
18
|
* CDN build externalises core, so a core change could not be seen through
|
|
19
19
|
* `?clientBundleUrl=` until it had been published. 0223 always assigned it
|
|
20
20
|
* here, and this is that move.
|
|
21
|
+
*
|
|
22
|
+
* WHAT THIS PHRASE IS FOR, AND WHY IT IS SHORT (ADR 0238)
|
|
23
|
+
* ------------------------------------------------------
|
|
24
|
+
* It buys **recall, and only recall**. In media-manager an editorial slot
|
|
25
|
+
* retrieves `SEMANTIC` over `semantic_text` capped at 30 candidates
|
|
26
|
+
* (`CandidateGather.planFor`), scoped by a filter that is just "this site's
|
|
27
|
+
* images" (`buildFilter`) — so the phrase ranks the corpus rather than gating
|
|
28
|
+
* it. Which picture then WINS is decided by `SlotScorer`, whose every term is
|
|
29
|
+
* geometric (crop fit, background, subject, text safety, resolution). Vespa's
|
|
30
|
+
* relevance score is not one of them, and the pool's order is discarded by the
|
|
31
|
+
* assignment solve. The phrase's entire job is choosing which 30 images reach
|
|
32
|
+
* the scorer.
|
|
33
|
+
*
|
|
34
|
+
* That is why it is budgeted in WORDS. The documents it is matched against are
|
|
35
|
+
* short — `ImageDocumentWriter.semanticText` embeds `alt, caption, label,
|
|
36
|
+
* label…`, usually under ten words — and retrieval is hybrid, BM25 blended with
|
|
37
|
+
* vector closeness. A headline plus a clause of marketing prose is the wrong
|
|
38
|
+
* shape against that on both halves: the vector drifts toward the mean of
|
|
39
|
+
* several topics instead of sitting on the subject, and BM25 spends term weight
|
|
40
|
+
* on filler that pulls in whatever else on the site happens to carry those
|
|
41
|
+
* words. Nothing normalises the query — case folding and dedupe (`LabelMerge`)
|
|
42
|
+
* happen on the document side only.
|
|
43
|
+
*
|
|
44
|
+
* A phrase that is too specific costs a worse 30, never a blank section: the
|
|
45
|
+
* site-wide widening in `CandidateGather.poolFor` is entity-only because an
|
|
46
|
+
* editorial pool empties only when the site has no images at all.
|
|
47
|
+
*
|
|
48
|
+
* CHANGING ANY OF THIS INVALIDATES SLOT IDS. `useImageSlot` derives a text
|
|
49
|
+
* slot's id as `s:${semantic}` (ADR 0223), so the phrase IS the cache key —
|
|
50
|
+
* every text-derived slot in the product gets a new id. Not a place for
|
|
51
|
+
* cosmetic edits.
|
|
21
52
|
*/
|
|
22
53
|
export interface ComposeSemanticInput {
|
|
23
54
|
/** The section's headline. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"composeSemantic.d.ts","sourceRoot":"","sources":["../../../src/image/composeSemantic.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"composeSemantic.d.ts","sourceRoot":"","sources":["../../../src/image/composeSemantic.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AAmFH,MAAM,WAAW,oBAAoB;IACnC,8BAA8B;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uCAAuC;IACvC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;yEAEqE;IACrE,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,EAC9B,KAAK,EACL,IAAI,EACJ,eAAe,GAChB,EAAE,oBAAoB,GAAG,MAAM,CA4B/B"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/web5-core",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "1.63.
|
|
4
|
+
"version": "1.63.37",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "tsachis",
|
|
7
7
|
"email": "tsachis@wix.com"
|
|
@@ -100,5 +100,5 @@
|
|
|
100
100
|
"wallaby": {
|
|
101
101
|
"autoDetect": true
|
|
102
102
|
},
|
|
103
|
-
"falconPackageHash": "
|
|
103
|
+
"falconPackageHash": "13189ccec55e96d863674e8bdad119b67199e59b89283f33fe077e91"
|
|
104
104
|
}
|