@nodaro/prompts 1.2.1 → 1.3.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodaro/prompts",
3
- "version": "1.2.1",
3
+ "version": "1.3.1",
4
4
  "description": "Nodaro's prompt-engineering layer — person/picker catalogs with prompt hints, identity-lock clauses, entity prompt builders, brand presets, and prompt/reference assembly shared by the Nodaro platform and SDK.",
5
5
  "type": "module",
6
6
  "license": "FSL-1.1-Apache-2.0",
@@ -0,0 +1,61 @@
1
+ import { describe, it, expect } from "vitest"
2
+ import { buildPersonHints } from "../person.js"
3
+ import { buildStylingHints } from "../styling.js"
4
+
5
+ /**
6
+ * Content-rejection hardening (app_reports batch, 2026-07-18): skin-exposure
7
+ * and lipstick clauses COMPOUND in providers' output-safety classifiers when
8
+ * the analyzer fills several overlapping dimensions from one photo. These
9
+ * tests pin the two de-stacking rules.
10
+ */
11
+
12
+ describe("exposure hint collapsing", () => {
13
+ it("midriff + navel together fold into ONE neutral clause", () => {
14
+ const hints = buildPersonHints({
15
+ distinctiveFeature: ["feature-midriff-visible", "feature-navel-visible"],
16
+ } as never)
17
+ const exposure = hints.filter((h) => /midriff|navel|stomach/i.test(h))
18
+ expect(exposure).toEqual(["wearing a cropped style, midriff and navel visible"])
19
+ })
20
+
21
+ it("each alone keeps its own (softened, garment-language) hint", () => {
22
+ const midriffOnly = buildPersonHints({
23
+ distinctiveFeature: ["feature-midriff-visible"],
24
+ } as never)
25
+ expect(midriffOnly).toContain("wearing a cropped style with the midriff visible")
26
+ expect(midriffOnly.join(" ")).not.toMatch(/bare stomach/i)
27
+
28
+ const navelOnly = buildPersonHints({
29
+ distinctiveFeature: ["feature-navel-visible"],
30
+ } as never)
31
+ expect(navelOnly).toContain("with the navel visible")
32
+ expect(navelOnly.join(" ")).not.toMatch(/bare stomach/i)
33
+ })
34
+
35
+ it("other distinctive features still emit alongside the collapsed pair", () => {
36
+ const hints = buildPersonHints({
37
+ distinctiveFeature: [
38
+ "feature-midriff-visible",
39
+ "feature-navel-visible",
40
+ "feature-freckles",
41
+ ],
42
+ } as never)
43
+ expect(hints.filter((h) => /midriff/i.test(h))).toHaveLength(1)
44
+ expect(hints.length).toBeGreaterThan(1)
45
+ })
46
+ })
47
+
48
+ describe("bold-lips cross-catalog dedupe", () => {
49
+ it("skips makeup-bold-lips when the shared map already carries lip-state-bold-red", () => {
50
+ const hints = buildStylingHints({
51
+ makeup: "makeup-bold-lips",
52
+ lipState: ["lip-state-bold-red"],
53
+ } as never)
54
+ expect(hints.join(" ")).not.toMatch(/bold lips/i)
55
+ })
56
+
57
+ it("keeps makeup-bold-lips without the person clause (separate-node consumers)", () => {
58
+ const hints = buildStylingHints({ makeup: "makeup-bold-lips" } as never)
59
+ expect(hints.join(" ")).toMatch(/bold lips/i)
60
+ })
61
+ })
package/src/person.ts CHANGED
@@ -708,8 +708,8 @@ export const PEOPLE: ReadonlyArray<Person> = [
708
708
  { id: "feature-nostril-piercing", label: "Nostril Piercing", dimension: "distinctive-features", description: "Single nostril stud or ring", promptHint: "with a single nostril piercing, a small stud or ring at the nostril" },
709
709
  { id: "feature-bare-shoulders", label: "Bare Shoulders", dimension: "distinctive-features", description: "Bare shoulders exposed", promptHint: "with bare shoulders exposed, the line of the collarbone and shoulder muscles uncovered" },
710
710
  { id: "feature-collarbone-visible", label: "Collarbone Visible", dimension: "distinctive-features", description: "Prominent collarbone catching light", promptHint: "with a prominent collarbone clearly defined and catching the light" },
711
- { id: "feature-midriff-visible", label: "Midriff Visible", dimension: "distinctive-features", description: "Exposed midriff between top and bottom", promptHint: "with the midriff exposed, a strip of bare stomach visible between the top and the bottom" },
712
- { id: "feature-navel-visible", label: "Navel Visible", dimension: "distinctive-features", description: "Visible navel on bare stomach", promptHint: "with a visible navel on a bare stomach" },
711
+ { id: "feature-midriff-visible", label: "Midriff Visible", dimension: "distinctive-features", description: "Exposed midriff between top and bottom", promptHint: "wearing a cropped style with the midriff visible" },
712
+ { id: "feature-navel-visible", label: "Navel Visible", dimension: "distinctive-features", description: "Visible navel on bare stomach", promptHint: "with the navel visible" },
713
713
  { id: "feature-elongated-neck", label: "Elongated Neck", dimension: "distinctive-features", description: "Long swan-like neck", promptHint: "with an elongated, swan-like neck, long and gracefully extended" },
714
714
  { id: "feature-under-eye-circles", label: "Under-Eye Circles", dimension: "distinctive-features", description: "Subtle dark circles under the eyes (distinct from puffy eye-bags)", promptHint: "with subtle dark circles under the eyes" },
715
715
  { id: "feature-fangs", label: "Fangs", dimension: "distinctive-features", description: "Visible fangs (vampire / character archetype)", promptHint: "with visible fangs at the canines, character / vampire archetype" },
@@ -1242,7 +1242,18 @@ export function buildAgeHint(
1242
1242
  function emitIndependentHints(value: unknown): string[] {
1243
1243
  const ids = normalizePickIds(value)
1244
1244
  const out: string[] = []
1245
+ // Skin-exposure clauses COMPOUND in providers' output-safety classifiers:
1246
+ // "midriff visible" + "navel visible" as separate clauses reads twice as
1247
+ // exposed as it is, and stacked with glamour hints it tips borderline draws
1248
+ // into content blocks (observed on kie-standard, 2026-07-18 app_reports).
1249
+ // When both are picked, fold them into ONE neutral clause.
1250
+ const midriff = ids.includes("feature-midriff-visible")
1251
+ const navel = ids.includes("feature-navel-visible")
1245
1252
  for (const id of ids) {
1253
+ if (midriff && navel && (id === "feature-midriff-visible" || id === "feature-navel-visible")) {
1254
+ if (id === "feature-midriff-visible") out.push("wearing a cropped style, midriff and navel visible")
1255
+ continue
1256
+ }
1246
1257
  const hint = getPersonPromptHint(id)
1247
1258
  if (hint) out.push(hint)
1248
1259
  }
package/src/styling.ts CHANGED
@@ -100,6 +100,7 @@ export const STYLINGS: ReadonlyArray<Styling> = [
100
100
  { id: "headwear-bandana", label: "Bandana", dimension: "headwear", description: "Bandana tied around head", promptHint: "wearing a bandana tied around the head" },
101
101
  { id: "headwear-hood", label: "Hood", dimension: "headwear", description: "Hooded sweatshirt up", promptHint: "with a hood pulled up over the head" },
102
102
  { id: "headwear-crown", label: "Crown", dimension: "headwear", description: "Royal or decorative crown", promptHint: "wearing a crown atop the head" },
103
+ { id: "headwear-nemes", label: "Nemes Headdress", dimension: "headwear", description: "Striped pharaonic nemes with uraeus", promptHint: "wearing a striped pharaonic nemes headdress with a golden uraeus cobra at the brow" },
103
104
  { id: "headwear-helmet", label: "Helmet", dimension: "headwear", description: "Protective helmet", promptHint: "wearing a helmet" },
104
105
  { id: "headwear-veil", label: "Veil", dimension: "headwear", description: "Veil draped over face/hair", promptHint: "wearing a veil draped over the head" },
105
106
  { id: "headwear-bucket-hat", label: "Bucket Hat", dimension: "headwear", description: "Soft-brimmed casual bucket hat", promptHint: "wearing a soft-brimmed casual bucket hat" },
@@ -218,6 +219,7 @@ export const STYLINGS: ReadonlyArray<Styling> = [
218
219
  { id: "face-paint-dramatic", label: "Dramatic Face Paint", dimension: "face-paint", description: "Bold artistic face paint", promptHint: "with dramatic, artistic face paint covering portions of the face" },
219
220
  { id: "face-paint-costume", label: "Costume Paint", dimension: "face-paint", description: "Cosplay / theatrical paint", promptHint: "with theatrical costume face paint" },
220
221
  { id: "face-paint-tribal", label: "Tribal Markings", dimension: "face-paint", description: "Tribal-inspired face markings", promptHint: "with tribal-inspired face markings" },
222
+ { id: "face-paint-flag", label: "Flag Face Paint", dimension: "face-paint", description: "National flag painted on cheeks, sports-fan", promptHint: "with a national flag painted on the cheeks, sports-fan face paint" },
221
223
  { id: "face-paint-warpaint", label: "War Paint", dimension: "face-paint", description: "Warrior war paint streaks", promptHint: "with warrior war paint streaked across the face" },
222
224
 
223
225
  // -------------------- Fabric (clothing material) --------------------
@@ -248,12 +250,14 @@ export const STYLINGS: ReadonlyArray<Styling> = [
248
250
  { id: "outfit-evening-gown", label: "Evening Gown", dimension: "outfit", description: "Floor-length formal gown", promptHint: "dressed in a floor-length evening gown with a fitted bodice and a flowing skirt" },
249
251
  { id: "outfit-wedding-dress", label: "Wedding Dress", dimension: "outfit", description: "White wedding gown", promptHint: "dressed in a white wedding gown with intricate lace detailing and a sweeping train" },
250
252
  { id: "outfit-cocktail-dress", label: "Cocktail Dress", dimension: "outfit", description: "Knee-length cocktail dress", promptHint: "dressed in a sleek knee-length cocktail dress" },
253
+ { id: "outfit-sundress", label: "Sundress / Maxi", dimension: "outfit", description: "Flowy halter-neck sundress / patterned maxi", promptHint: "dressed in a flowy halter-neck sundress with a patterned maxi-length skirt" },
251
254
  { id: "outfit-lab-coat", label: "Lab Coat", dimension: "outfit", description: "White medical lab coat", promptHint: "wearing a crisp white lab coat layered over the rest of the clothing" },
252
255
  { id: "outfit-scrubs", label: "Medical Scrubs", dimension: "outfit", description: "Solid-color medical scrubs", promptHint: "dressed in solid-color medical scrubs — loose-fit top and matching drawstring pants" },
253
256
  { id: "outfit-military", label: "Military Fatigues", dimension: "outfit", description: "Olive-drab military fatigues", promptHint: "dressed in olive-drab military fatigues with utility pockets and a sturdy belt" },
254
257
  { id: "outfit-lifeguard", label: "Lifeguard Outfit", dimension: "outfit", description: "Red lifeguard top and shorts", promptHint: "dressed in a bright red lifeguard outfit — a fitted top and matching shorts" },
255
258
  { id: "outfit-athleisure", label: "Athleisure Set", dimension: "outfit", description: "Matching athleisure top and bottom", promptHint: "dressed in a matching athleisure set — a fitted top and leggings in the same tone" },
256
259
  { id: "outfit-tracksuit", label: "Tracksuit", dimension: "outfit", description: "Two-piece zip-front tracksuit", promptHint: "dressed in a two-piece tracksuit — a zip-front jacket and matching joggers" },
260
+ { id: "outfit-soccer-jersey", label: "Soccer Jersey", dimension: "outfit", description: "National-team football jersey with crest", promptHint: "wearing a national-team soccer jersey — team colors and trim with the team crest on the chest" },
257
261
  { id: "outfit-bikini", label: "Bikini", dimension: "outfit", description: "Two-piece swimsuit", promptHint: "in a two-piece bikini" },
258
262
  { id: "outfit-one-piece-swim", label: "One-Piece Swimsuit", dimension: "outfit", description: "Fitted one-piece swimsuit", promptHint: "in a fitted one-piece swimsuit" },
259
263
  { id: "outfit-lingerie", label: "Lingerie Set", dimension: "outfit", description: "Matching lingerie set", promptHint: "in a matching lingerie set" },
@@ -273,6 +277,7 @@ export const STYLINGS: ReadonlyArray<Styling> = [
273
277
  { id: "outfit-pirate", label: "Pirate", dimension: "outfit", description: "Pirate costume: tricorn hat, sash, frilly shirt, boots", promptHint: "wearing a pirate costume — a tricorn hat, a frilly shirt with a sash at the waist, and tall leather boots" },
274
278
  { id: "outfit-fairy", label: "Fairy", dimension: "outfit", description: "Fantasy fairy: gauzy wings, flower crown, ethereal dress", promptHint: "wearing a fantasy fairy costume — gauzy translucent wings, a flower crown, and an ethereal flowing dress" },
275
279
  { id: "outfit-mermaid", label: "Mermaid", dimension: "outfit", description: "Fantasy mermaid: scaled tail/skirt, shell top, flowing hair", promptHint: "wearing a fantasy mermaid costume — a scaled tail or fitted scaled skirt, a shell top, and long flowing hair" },
280
+ { id: "outfit-pharaoh", label: "Pharaoh Regalia", dimension: "outfit", description: "Ancient Egyptian royalty: usekh collar, pectoral, pleated kilt", promptHint: "wearing ancient Egyptian pharaoh regalia — a broad beaded usekh collar, a jeweled falcon pectoral, and a pleated linen shendyt kilt with golden arm cuffs" },
276
281
 
277
282
  // -------------------- Top (upper-body garment) --------------------
278
283
  { id: "top-tshirt", label: "T-Shirt", dimension: "top", description: "Plain crewneck t-shirt", promptHint: "wearing a fitted plain crewneck t-shirt with short sleeves" },
@@ -385,6 +390,8 @@ export const STYLINGS: ReadonlyArray<Styling> = [
385
390
  { id: "state-tucked-in", label: "Tucked In", dimension: "wardrobe-state", description: "Top tucked into bottom", promptHint: "the top neatly tucked into the bottom" },
386
391
  { id: "state-half-tucked", label: "Half-Tucked", dimension: "wardrobe-state", description: "Front-tucked, back loose", promptHint: "the top half-tucked — front neatly tucked in, back hanging loose" },
387
392
  { id: "state-off-shoulder", label: "Off-Shoulder", dimension: "wardrobe-state", description: "Slipped off one shoulder", promptHint: "the top slipped off one shoulder, baring the skin and collarbone" },
393
+ { id: "state-halter-neck", label: "Halter Neck", dimension: "wardrobe-state", description: "Straps tied behind the neck", promptHint: "the top cut as a halter — straps tied behind the neck, shoulders and upper back bare" },
394
+ { id: "state-plunging-neck", label: "Plunging Neckline", dimension: "wardrobe-state", description: "Deep plunging V neckline", promptHint: "the neckline cut in a deep plunging V" },
388
395
  { id: "state-unbuttoned", label: "Unbuttoned", dimension: "wardrobe-state", description: "Open / unbuttoned", promptHint: "the outerwear hanging open and unbuttoned, falling loose at the sides" },
389
396
  { id: "state-rolled-sleeves", label: "Rolled Sleeves", dimension: "wardrobe-state", description: "Sleeves rolled up forearm", promptHint: "the sleeves rolled up to the forearm" },
390
397
  { id: "state-layered", label: "Layered", dimension: "wardrobe-state", description: "Multiple stacked layers", promptHint: "the outfit composed of multiple stacked layers, each piece visible at the edges" },
@@ -561,9 +568,21 @@ export function buildStylingHints(
561
568
  const pre = typeof data.preText === "string" ? data.preText.trim() : ""
562
569
  if (pre) hints.push(pre)
563
570
 
571
+ // The Person catalog's `lip-state-bold-red` and this catalog's
572
+ // `makeup-bold-lips` say the same thing; consumers that fold BOTH pickers
573
+ // off one shared value map (person.nodaro.ai's subject fold) double the
574
+ // lipstick clause. When the shared map shows the person clause already
575
+ // covers it, skip the makeup twin. Separate-node consumers (platform
576
+ // workflows) don't carry `lipState` here, so this no-ops for them.
577
+ const lipStateRaw = (data as Record<string, unknown>).lipState
578
+ const boldRedAlready = Array.isArray(lipStateRaw)
579
+ ? lipStateRaw.includes("lip-state-bold-red")
580
+ : lipStateRaw === "lip-state-bold-red"
581
+
564
582
  for (const dimension of STYLING_DIMENSION_ORDER) {
565
583
  const field = STYLING_FIELD_BY_DIMENSION[dimension]
566
584
  const raw = data[field]
585
+ if (dimension === "makeup" && boldRedAlready && raw === "makeup-bold-lips") continue
567
586
  // jewelry / wardrobe-state / hair-state are multi-pick (string | string[]);
568
587
  // emit each id's hint independently and let the comma-join compose.
569
588
  if (typeof raw === "string" && raw.length > 0) {