@nodaro/prompts 1.14.0 → 1.16.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.
Files changed (69) hide show
  1. package/dist/index.cjs +680 -251
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +406 -1
  4. package/dist/index.d.ts +406 -1
  5. package/dist/index.js +649 -253
  6. package/dist/index.js.map +1 -1
  7. package/package.json +2 -2
  8. package/src/__tests__/adult-only-ratchet.test.ts +110 -0
  9. package/src/__tests__/age-floor.test.ts +347 -0
  10. package/src/__tests__/catalog-id-guard.test.ts +146 -0
  11. package/src/__tests__/catalog-overlay.test.ts +163 -0
  12. package/src/__tests__/doctrine-roster-completeness.test.ts +1 -0
  13. package/src/__tests__/dod-replace-pack-acceptance.test.ts +17 -0
  14. package/src/__tests__/fixtures/parameter-hint-golden.json +315 -37
  15. package/src/__tests__/minor-age-floor.test.ts +136 -0
  16. package/src/__tests__/person-body-axes.test.ts +2 -2
  17. package/src/__tests__/person-exposure-hints.test.ts +59 -4
  18. package/src/__tests__/person-pack-adult-only.test.ts +45 -0
  19. package/src/__tests__/picker-analyzer-registry.test.ts +14 -0
  20. package/src/__tests__/retired-hint-strings.test.ts +68 -0
  21. package/src/__tests__/subject-registry.test.ts +1 -1
  22. package/src/__tests__/w1b-rephrase.test.ts +119 -0
  23. package/src/action-fx.ts +2 -1
  24. package/src/aesthetic.ts +2 -1
  25. package/src/age-floor.ts +359 -0
  26. package/src/age-signal.ts +36 -0
  27. package/src/atmosphere.ts +2 -1
  28. package/src/backdrop.ts +2 -1
  29. package/src/camera-format.ts +2 -1
  30. package/src/camera-motions.ts +2 -1
  31. package/src/catalog-id-guard.ts +189 -0
  32. package/src/catalog-overlay.ts +148 -0
  33. package/src/catalog-packs.ts +27 -2
  34. package/src/character-fx.ts +2 -1
  35. package/src/color-look.ts +2 -1
  36. package/src/composition-effects.ts +2 -1
  37. package/src/era.ts +2 -1
  38. package/src/exposure-settings.ts +2 -1
  39. package/src/framing.ts +2 -1
  40. package/src/held-prop.ts +2 -1
  41. package/src/index.ts +4 -0
  42. package/src/instrumentation.ts +5 -4
  43. package/src/lens.ts +2 -1
  44. package/src/lighting.ts +2 -1
  45. package/src/loop-subject.ts +3 -1
  46. package/src/materials.ts +2 -1
  47. package/src/mood.ts +16 -5
  48. package/src/music-genre.ts +5 -4
  49. package/src/music-mood.ts +4 -3
  50. package/src/parameter-prompt-hint.ts +13 -57
  51. package/src/person-packs.ts +14 -2
  52. package/src/person.ts +76 -42
  53. package/src/photo-genre.ts +14 -3
  54. package/src/photographer.ts +2 -1
  55. package/src/picker-analyzer-registry.ts +9 -0
  56. package/src/picker-catalogs.ts +11 -0
  57. package/src/pose.ts +17 -6
  58. package/src/post-process-effects.ts +2 -1
  59. package/src/prompt-builder.ts +3 -3
  60. package/src/render-quality.ts +2 -1
  61. package/src/setting.ts +2 -1
  62. package/src/shared-catalog-overlay.ts +83 -0
  63. package/src/style.ts +16 -1
  64. package/src/styling.ts +93 -29
  65. package/src/subject-registry.ts +2 -2
  66. package/src/temporal.ts +2 -1
  67. package/src/transitions.ts +2 -1
  68. package/src/voice-character.ts +6 -5
  69. package/src/voice-delivery.ts +4 -3
@@ -0,0 +1,163 @@
1
+ /**
2
+ * The resolver-side half of catalog curation: a pack that removes or rewrites
3
+ * an entry must change what the id→entry GETTERS return, because those are
4
+ * what turns a picker id into prompt text at run time. Before this overlay
5
+ * existed, a deployment's curation was visible in /v1/catalogs and invisible
6
+ * in every generated prompt.
7
+ *
8
+ * The totality test at the bottom is the one that keeps this fixed: every
9
+ * catalog with a getter must honor a deny pack, so a new catalog cannot ship
10
+ * with a getter that reads its base constant straight.
11
+ */
12
+ import { afterEach, describe, expect, it } from "vitest"
13
+ import {
14
+ registerCatalogPack,
15
+ resetCatalogPacks,
16
+ getPickerCatalog,
17
+ PICKER_CATALOGS,
18
+ getSetting,
19
+ getSettingPromptHint,
20
+ getPose,
21
+ getPosePromptHint,
22
+ getParameterPromptHint,
23
+ renderSubjectHints,
24
+ SETTINGS,
25
+ POSES,
26
+ } from "../index.js"
27
+ import { ANIMALS } from "@nodaro/shared"
28
+
29
+ afterEach(() => resetCatalogPacks())
30
+
31
+ describe("overlayEntry — the four outcomes", () => {
32
+ const settingId = SETTINGS[0].id
33
+
34
+ it("no packs: the base entry, by identity (mainline byte-identical)", () => {
35
+ expect(getSetting(settingId)).toBe(SETTINGS[0])
36
+ })
37
+
38
+ it("deny pack: the id resolves to NOTHING on the prompt path", () => {
39
+ registerCatalogPack({ id: "t-deny", catalogId: "setting", mode: "deny", denyIds: [settingId] })
40
+ expect(getSetting(settingId)).toBeUndefined()
41
+ expect(getSettingPromptHint(settingId)).toBe("")
42
+ // A sibling the pack did not touch still resolves.
43
+ expect(getSetting(SETTINGS[1].id)).toBeDefined()
44
+ })
45
+
46
+ it("replace pack with a REWRITE: the composed wording reaches the getter", () => {
47
+ const base = getPickerCatalog("setting")!
48
+ const rewritten = {
49
+ ...base,
50
+ options: base.options!.map((o) =>
51
+ o.id === settingId ? { ...o, label: "Curated Label", promptHint: "curated prompt text" } : o,
52
+ ),
53
+ }
54
+ registerCatalogPack({ id: "t-replace", catalogId: "setting", mode: "replace", catalog: rewritten })
55
+ const s = getSetting(settingId)!
56
+ expect(s.label).toBe("Curated Label")
57
+ expect(s.promptHint).toBe("curated prompt text")
58
+ expect(getSettingPromptHint(settingId)).toBe("curated prompt text")
59
+ // Fields the composed option does not carry survive from the base entry.
60
+ expect(s.id).toBe(settingId)
61
+ })
62
+
63
+ it("replace pack that OMITS an entry: the omitted id resolves to nothing", () => {
64
+ const base = getPickerCatalog("setting")!
65
+ registerCatalogPack({
66
+ id: "t-replace-omit",
67
+ catalogId: "setting",
68
+ mode: "replace",
69
+ catalog: { ...base, options: base.options!.filter((o) => o.id !== settingId) },
70
+ })
71
+ expect(getSetting(settingId)).toBeUndefined()
72
+ })
73
+
74
+ it("extend pack: a pack-added id resolves to the composed option", () => {
75
+ registerCatalogPack({
76
+ id: "t-extend",
77
+ catalogId: "setting",
78
+ mode: "extend",
79
+ options: [{ id: "curated-only", label: "Curated Only", promptHint: "only in the pack", term: "curated" }],
80
+ })
81
+ expect(getSetting("curated-only")?.promptHint).toBe("only in the pack")
82
+ })
83
+ })
84
+
85
+ describe("the dispatcher honors the overlay end to end", () => {
86
+ it("a denied pose sub-pick contributes nothing to the hint", () => {
87
+ const poseId = POSES[0].id
88
+ const before = getParameterPromptHint({ id: "n", type: "pose", data: { pose: poseId } } as never)
89
+ expect(before).not.toBe("")
90
+ registerCatalogPack({ id: "t-pose", catalogId: "pose", mode: "deny", denyIds: [poseId] })
91
+ expect(getPose(poseId)).toBeUndefined()
92
+ expect(getPosePromptHint(poseId)).toBe("")
93
+ expect(getParameterPromptHint({ id: "n", type: "pose", data: { pose: poseId } } as never)).toBe("")
94
+ })
95
+
96
+ it("the @nodaro/shared-owned catalogs are overlaid at the dispatcher", () => {
97
+ const animalId = ANIMALS[0].id
98
+ const before = getParameterPromptHint({ id: "n", type: "animal", data: { animal: animalId } } as never)
99
+ expect(before).not.toBe("")
100
+ registerCatalogPack({ id: "t-animal", catalogId: "animals", mode: "deny", denyIds: [animalId] })
101
+ expect(getParameterPromptHint({ id: "n", type: "animal", data: { animal: animalId } } as never)).toBe("")
102
+ })
103
+ })
104
+
105
+ describe("TOTALITY — every catalog honors a deny pack ON THE PROMPT PATH", () => {
106
+ // Mechanical, not enumerated: walk every catalog, deny its first id, and
107
+ // assert the dispatcher produces NOTHING for a node carrying that id — the
108
+ // same call the orchestrator makes. A getter that still reads its base
109
+ // constant straight fails here whether or not anyone remembered to list it.
110
+ // The field the id lives in comes from the catalog itself (single-dim
111
+ // valueField / multi-dim dimension field), so a catalog cannot slip past by
112
+ // storing its value under a name this test did not know.
113
+ const cases = PICKER_CATALOGS.map((c) => {
114
+ const dim = c.dimensions?.[0]
115
+ const field = c.valueField ?? dim?.field
116
+ // The first REAL entry: a catalog's "auto"/"none" no-op (term "") injects
117
+ // nothing by design, which would make the pre-deny assertion vacuous.
118
+ const opt = (c.options ?? dim?.options ?? []).find((o) => o.term !== "")
119
+ return { nodeType: c.nodeType, catalogId: c.catalogId, field: field!, id: opt?.id ?? "" }
120
+ })
121
+
122
+ it.each(cases)("$nodeType", ({ nodeType, catalogId, field, id }) => {
123
+ expect(id, `${catalogId}: catalog has no options to deny`).not.toBe("")
124
+ // `instruments` is the one field whose node value is always an array.
125
+ const node = { id: "n", type: nodeType, data: { [field]: field === "instruments" ? [id] : id } } as never
126
+ const before = getParameterPromptHint(node)
127
+ expect(before, `${nodeType}: the stock id must produce SOME hint for the test to mean anything`).not.toBe("")
128
+ registerCatalogPack({ id: `tot-${catalogId}`, catalogId, mode: "deny", denyIds: [id] })
129
+ expect(getParameterPromptHint(node), `${nodeType}: denied "${id}" still reaches the prompt`).toBe("")
130
+ })
131
+
132
+ it("a REWRITE reaches the prompt text end to end, for a prompts-owned and a shared-owned catalog", () => {
133
+ for (const [nodeType, field] of [["setting", "setting"], ["animal", "animal"]] as const) {
134
+ resetCatalogPacks()
135
+ const cat = getPickerCatalog(nodeType)!
136
+ const id = cat.options![0].id
137
+ const stock = getParameterPromptHint({ id: "n", type: nodeType, data: { [field]: id } } as never)
138
+ registerCatalogPack({
139
+ id: `rw-${nodeType}`,
140
+ catalogId: cat.catalogId,
141
+ mode: "replace",
142
+ catalog: { ...cat, options: cat.options!.map((o) => (o.id === id ? { ...o, promptHint: "CURATED WORDING" } : o)) },
143
+ })
144
+ const curated = getParameterPromptHint({ id: "n", type: nodeType, data: { [field]: id } } as never)
145
+ expect(curated, nodeType).toContain("CURATED WORDING")
146
+ expect(curated, nodeType).not.toBe(stock)
147
+ }
148
+ })
149
+
150
+ it("the same animal id renders identically through the parameter node and the subject fold", () => {
151
+ const id = getPickerCatalog("animals")!.options![0].id
152
+ registerCatalogPack({
153
+ id: "rw-animal-subject",
154
+ catalogId: "animals",
155
+ mode: "replace",
156
+ catalog: { ...getPickerCatalog("animals")!, options: getPickerCatalog("animals")!.options!.map((o) => (o.id === id ? { ...o, promptHint: "SUBJECT WORDING" } : o)) },
157
+ })
158
+ const viaNode = getParameterPromptHint({ id: "n", type: "animal", data: { animal: id } } as never)
159
+ expect(viaNode).toContain("SUBJECT WORDING")
160
+ const viaSubject = renderSubjectHints({ animal: id }, "image")
161
+ expect(JSON.stringify(viaSubject)).toContain("SUBJECT WORDING")
162
+ })
163
+ })
@@ -26,6 +26,7 @@ const DELIBERATELY_GENERIC: ReadonlyMap<string, string> = new Map([
26
26
  ["minimax", "legacy 5s tier, no vendor guide"],
27
27
  ["seedance", "legacy Seedance 1.x, superseded"],
28
28
  ["ltx-2.3-fast", "no vendor prompt guide published"],
29
+ ["ltx-2.3-pro", "no vendor prompt guide published"],
29
30
  ["sora2", "roster utility — no first-party guide via KIE"],
30
31
  ["sora2-pro", "roster utility — no first-party guide via KIE"],
31
32
  ])
@@ -39,3 +39,20 @@ describe("DoD — adopt a vendored replacement pack by registration", () => {
39
39
  expect(JSON.stringify(PICKER_CATALOGS)).toBe(upstreamSnapshot)
40
40
  })
41
41
  })
42
+
43
+ describe("adultOnly survives packs (W1-a)", () => {
44
+ it("a replace pack keeps the flag on its options; an extend pack adds a flagged option", () => {
45
+ const upstreamMood = PICKER_CATALOGS.find((c) => c.catalogId === "mood")!
46
+ const vendored: PickerCatalog = {
47
+ ...upstreamMood,
48
+ options: [
49
+ { id: "calm", label: "Calm", promptHint: "with a calm mood", term: "calm", category: upstreamMood.options![0].category },
50
+ { id: "sultry", label: "Sultry", promptHint: "with a sultry expression", term: "sultry", category: upstreamMood.options![0].category, adultOnly: true },
51
+ ],
52
+ }
53
+ registerCatalogPack({ id: "sai/mood", catalogId: "mood", mode: "replace", catalog: vendored })
54
+ const out = getRegisteredPickerCatalogs().find((c) => c.catalogId === "mood")!.options!
55
+ expect(out.find((o) => o.id === "sultry")!.adultOnly).toBe(true)
56
+ expect(out.find((o) => o.id === "calm")!.adultOnly).toBeUndefined()
57
+ })
58
+ })