@nodaro/prompts 1.11.0 → 1.13.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.
- package/dist/index.cjs +627 -177
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +726 -33
- package/dist/index.d.ts +726 -33
- package/dist/index.js +598 -179
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/entity-convergence-image.test.ts.snap +19 -0
- package/src/__tests__/animal-getters-parity.test.ts +82 -0
- package/src/__tests__/assemble-image-input-cap.test.ts +236 -0
- package/src/__tests__/assemble-image-input.test.ts +100 -19
- package/src/__tests__/assemble-video-input-cap.test.ts +442 -0
- package/src/__tests__/assemble-video-input.test.ts +167 -33
- package/src/__tests__/direction-hint-token-safety.test.ts +21 -0
- package/src/__tests__/entity-convergence-image.test.ts +374 -0
- package/src/__tests__/location-convergence-image.test.ts +29 -1
- package/src/__tests__/location-default-role-image.test.ts +166 -0
- package/src/__tests__/mention-splice-spacing.test.ts +257 -0
- package/src/__tests__/prompt-style-section.test.ts +345 -0
- package/src/__tests__/read-node-subject.test.ts +140 -0
- package/src/__tests__/style-section-boundary.test.ts +179 -0
- package/src/__tests__/subject-fold.test.ts +251 -0
- package/src/__tests__/subject-registry.test.ts +312 -0
- package/src/assemble-image-input.ts +169 -41
- package/src/assemble-video-input.ts +200 -25
- package/src/direction-registry.ts +116 -28
- package/src/hint-shedding.ts +87 -0
- package/src/index.ts +3 -0
- package/src/parameter-prompt-hint.ts +8 -7
- package/src/picker-catalogs.ts +14 -7
- package/src/prompt-builder.ts +628 -88
- package/src/prompt-hint-join.ts +9 -0
- package/src/prompt-style-section.ts +256 -0
- package/src/read-node-direction.ts +60 -1
- package/src/subject-registry.ts +464 -0
- package/src/video-reference-resolver.ts +5 -2
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest"
|
|
2
2
|
import { composeVideoPromptText } from "../assemble-video-input.js"
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
FILM_STYLE_KEYS,
|
|
5
|
+
VIDEO_HINT_MODE_DEFAULT,
|
|
6
|
+
directionFieldsForSurface,
|
|
7
|
+
renderDirectionHints,
|
|
8
|
+
} from "../direction-registry.js"
|
|
4
9
|
import { getStylePromptHint, getStyleTerm } from "../style.js"
|
|
5
10
|
import { getTransitionPromptHint, getTransitionTerm } from "../transitions.js"
|
|
6
11
|
import { getCameraMotionPromptHint, getCameraMotionTerm } from "../camera-motions.js"
|
|
@@ -25,6 +30,10 @@ import { renderStructuredFields } from "../prompt-builder-structured-fields.js"
|
|
|
25
30
|
* 2. THE VERBOSITY POLICY — look dimensions render their full clause, motion
|
|
26
31
|
* dimensions their compact professional term. That split moved from the
|
|
27
32
|
* client to the platform, so it is pinned in both directions.
|
|
33
|
+
* 3. THE `[style]` SECTION — the look clauses read in a trailing block, not
|
|
34
|
+
* inline; the motion family and the structured fragment stay in the body.
|
|
35
|
+
* The header is written as a LITERAL in every expectation here so a typo in
|
|
36
|
+
* `STYLE_SECTION_HEADER` fails rather than redefining the contract.
|
|
28
37
|
*
|
|
29
38
|
* Real catalog ids throughout: every `get*PromptHint` returns `""` on a miss,
|
|
30
39
|
* so a made-up id would make most assertions vacuously pass.
|
|
@@ -62,9 +71,9 @@ describe("composeVideoPromptText — the no-op contract", () => {
|
|
|
62
71
|
})
|
|
63
72
|
|
|
64
73
|
describe("composeVideoPromptText — the verbosity policy", () => {
|
|
65
|
-
it("renders a LOOK dimension as its full clause", () => {
|
|
74
|
+
it("renders a LOOK dimension as its full clause, in the section", () => {
|
|
66
75
|
expect(composeVideoPromptText("a knight", { style: STYLE })).toBe(
|
|
67
|
-
`a knight
|
|
76
|
+
`a knight\n\n[style]:\n${getStylePromptHint(STYLE)}`,
|
|
68
77
|
)
|
|
69
78
|
})
|
|
70
79
|
|
|
@@ -91,12 +100,41 @@ describe("composeVideoPromptText — the verbosity policy", () => {
|
|
|
91
100
|
hintMode: "full",
|
|
92
101
|
}),
|
|
93
102
|
).toBe(`a knight. ${getTransitionPromptHint(TRANSITION)}`)
|
|
94
|
-
// …and "compact" demotes the look family to its term.
|
|
103
|
+
// …and "compact" demotes the look family to its term. Verbosity does not
|
|
104
|
+
// move a clause: the look term still reads in the section.
|
|
95
105
|
expect(
|
|
96
106
|
composeVideoPromptText("a knight", { style: STYLE }, undefined, {
|
|
97
107
|
hintMode: "compact",
|
|
98
108
|
}),
|
|
99
|
-
).toBe(`a knight
|
|
109
|
+
).toBe(`a knight\n\n[style]:\n${getStyleTerm(STYLE)}`)
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* THE BODY/SECTION BOUNDARY. Tal's rule: camera motion is part of the shot
|
|
115
|
+
* prose, not the style — so the WHOLE motion family stays in the body, and the
|
|
116
|
+
* boundary is the registry's `family` column, the same column the verbosity
|
|
117
|
+
* policy above splits on. Coupling them is deliberate; a row cannot be shot
|
|
118
|
+
* prose for one and look for the other.
|
|
119
|
+
*/
|
|
120
|
+
describe("composeVideoPromptText — what stays in the body", () => {
|
|
121
|
+
it("keeps every motion clause inline and emits no header for a motion-only fold", () => {
|
|
122
|
+
const out = composeVideoPromptText("a knight", {
|
|
123
|
+
cameraMotion: CAMERA_MOTION,
|
|
124
|
+
transition: TRANSITION,
|
|
125
|
+
})
|
|
126
|
+
expect(out).toBe(
|
|
127
|
+
`a knight. ${getCameraMotionTerm(CAMERA_MOTION)}. ${getTransitionTerm(TRANSITION)}`,
|
|
128
|
+
)
|
|
129
|
+
expect(out).not.toContain("[style]")
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it("splits a mixed fold: motion inline, look in the section", () => {
|
|
133
|
+
expect(
|
|
134
|
+
composeVideoPromptText("a knight", { cameraMotion: CAMERA_MOTION, style: STYLE }),
|
|
135
|
+
).toBe(
|
|
136
|
+
`a knight. ${getCameraMotionTerm(CAMERA_MOTION)}\n\n[style]:\n${getStylePromptHint(STYLE)}`,
|
|
137
|
+
)
|
|
100
138
|
})
|
|
101
139
|
})
|
|
102
140
|
|
|
@@ -114,22 +152,38 @@ describe("composeVideoPromptText — ordering", () => {
|
|
|
114
152
|
})
|
|
115
153
|
|
|
116
154
|
it("folds in TABLE order, not the caller's object order", () => {
|
|
117
|
-
// `shotSize` (row 2) precedes `
|
|
155
|
+
// `shotSize` (row 2) precedes `lightingStyle` (row 15) on the scene line
|
|
156
|
+
// however the object is written.
|
|
118
157
|
const out = composeVideoPromptText("a knight", {
|
|
119
|
-
|
|
158
|
+
lightingStyle: LIGHTING_STYLE,
|
|
120
159
|
shotSize: SHOT_SIZE,
|
|
121
160
|
})!
|
|
122
161
|
expect(out.indexOf(getFramingPromptHint(SHOT_SIZE))).toBeLessThan(
|
|
123
|
-
out.indexOf(
|
|
162
|
+
out.indexOf(getLightingPromptHint(LIGHTING_STYLE)),
|
|
124
163
|
)
|
|
125
164
|
})
|
|
126
165
|
|
|
127
|
-
it("
|
|
166
|
+
it("groups before it orders: the film line leads the scene line", () => {
|
|
167
|
+
// `shotSize` folds at row 2 and `style` at row 22, so table order alone
|
|
168
|
+
// would read the framing clause first. The section's two lines outrank it.
|
|
169
|
+
expect(composeVideoPromptText("a knight", { style: STYLE, shotSize: SHOT_SIZE })).toBe(
|
|
170
|
+
`a knight\n\n[style]:\n${getStylePromptHint(STYLE)}\n${getFramingPromptHint(SHOT_SIZE)}`,
|
|
171
|
+
)
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it("ends the BODY with the structured fragment, then hangs the section off it", () => {
|
|
128
175
|
const structured = { mood: "wistful" }
|
|
129
176
|
const fragment = renderStructuredFields(structured)
|
|
130
177
|
expect(fragment.length).toBeGreaterThan(0)
|
|
131
|
-
const out = composeVideoPromptText(
|
|
132
|
-
|
|
178
|
+
const out = composeVideoPromptText(
|
|
179
|
+
"a knight",
|
|
180
|
+
{ style: STYLE, cameraMotion: CAMERA_MOTION },
|
|
181
|
+
structured,
|
|
182
|
+
)!
|
|
183
|
+
expect(out).toBe(
|
|
184
|
+
`a knight. ${getCameraMotionTerm(CAMERA_MOTION)}. ${fragment}` +
|
|
185
|
+
`\n\n[style]:\n${getStylePromptHint(STYLE)}`,
|
|
186
|
+
)
|
|
133
187
|
})
|
|
134
188
|
})
|
|
135
189
|
|
|
@@ -138,7 +192,7 @@ describe("composeVideoPromptText — multi-pick doctrine", () => {
|
|
|
138
192
|
const blended = buildMoodHints({ mood: ["happy", "serene"] }, "full")
|
|
139
193
|
expect(blended).toHaveLength(1)
|
|
140
194
|
expect(composeVideoPromptText("a knight", { mood: ["happy", "serene"] })).toBe(
|
|
141
|
-
`a knight
|
|
195
|
+
`a knight\n\n[style]:\n${blended[0]}`,
|
|
142
196
|
)
|
|
143
197
|
})
|
|
144
198
|
|
|
@@ -147,7 +201,7 @@ describe("composeVideoPromptText — multi-pick doctrine", () => {
|
|
|
147
201
|
expect(blended.length).toBeGreaterThan(0)
|
|
148
202
|
expect(
|
|
149
203
|
composeVideoPromptText("a knight", { aesthetic: ["y2k", "cottagecore"] }),
|
|
150
|
-
).toBe(`a knight
|
|
204
|
+
).toBe(`a knight\n\n[style]:\n${blended}`)
|
|
151
205
|
})
|
|
152
206
|
|
|
153
207
|
it("slices an over-cap array to the dimension's maxPicks (atmosphere = 2)", () => {
|
|
@@ -156,7 +210,9 @@ describe("composeVideoPromptText — multi-pick doctrine", () => {
|
|
|
156
210
|
})!
|
|
157
211
|
const kept = buildAtmosphereHints(["clear", "cloudy"], "full")
|
|
158
212
|
expect(kept).toHaveLength(2)
|
|
159
|
-
|
|
213
|
+
// Both survivors share the scene line, `. `-joined exactly as an inline
|
|
214
|
+
// fold joined them.
|
|
215
|
+
expect(out).toBe(`a knight\n\n[style]:\n${kept.join(". ")}`)
|
|
160
216
|
expect(out).not.toContain(buildAtmosphereHints("overcast", "full")[0])
|
|
161
217
|
})
|
|
162
218
|
|
|
@@ -164,7 +220,7 @@ describe("composeVideoPromptText — multi-pick doctrine", () => {
|
|
|
164
220
|
// The legacy `V2LookPicker` shape: a single-pick dimension that stored an
|
|
165
221
|
// array. Must degrade to one hint, never throw and never drop the key.
|
|
166
222
|
const out = composeVideoPromptText("a knight", { style: [STYLE, "anime"] })
|
|
167
|
-
expect(out).toBe(`a knight
|
|
223
|
+
expect(out).toBe(`a knight\n\n[style]:\n${getStylePromptHint(STYLE)}`)
|
|
168
224
|
})
|
|
169
225
|
})
|
|
170
226
|
|
|
@@ -191,12 +247,31 @@ describe("composeVideoPromptText — tolerance", () => {
|
|
|
191
247
|
|
|
192
248
|
describe("composeVideoPromptText — an empty or absent body", () => {
|
|
193
249
|
it("returns the hints alone for an empty prompt (never a leading '. ')", () => {
|
|
194
|
-
expect(composeVideoPromptText("", {
|
|
250
|
+
expect(composeVideoPromptText("", { transition: TRANSITION })).toBe(
|
|
251
|
+
getTransitionTerm(TRANSITION),
|
|
252
|
+
)
|
|
195
253
|
})
|
|
196
254
|
|
|
197
255
|
it("returns the hints alone for an ABSENT prompt", () => {
|
|
198
|
-
expect(composeVideoPromptText(undefined, {
|
|
199
|
-
|
|
256
|
+
expect(composeVideoPromptText(undefined, { transition: TRANSITION })).toBe(
|
|
257
|
+
getTransitionTerm(TRANSITION),
|
|
258
|
+
)
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
it("drops the section's blank line with the body (never a leading newline)", () => {
|
|
262
|
+
const sectionOnly = `[style]:\n${getStylePromptHint(STYLE)}`
|
|
263
|
+
expect(composeVideoPromptText("", { style: STYLE })).toBe(sectionOnly)
|
|
264
|
+
expect(composeVideoPromptText(undefined, { style: STYLE })).toBe(sectionOnly)
|
|
265
|
+
expect(composeVideoPromptText(" ", { style: STYLE })).toBe(sectionOnly)
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
it("TRIMS a prompt the section is the only thing folded onto", () => {
|
|
269
|
+
// The section counts as "something folded", so the body is trimmed exactly
|
|
270
|
+
// as the hint-join branch trims it — otherwise the blank line would inherit
|
|
271
|
+
// the prompt's trailing whitespace. (With NO fold at all the prompt still
|
|
272
|
+
// comes back untrimmed: the no-op contract above.)
|
|
273
|
+
expect(composeVideoPromptText(" a knight \n", { style: STYLE })).toBe(
|
|
274
|
+
`a knight\n\n[style]:\n${getStylePromptHint(STYLE)}`,
|
|
200
275
|
)
|
|
201
276
|
})
|
|
202
277
|
})
|
|
@@ -208,30 +283,49 @@ describe("composeVideoPromptText — the dedupe invariant", () => {
|
|
|
208
283
|
it("emits ONE clause when a legacy and a canonical key carry the SAME id", () => {
|
|
209
284
|
expect(
|
|
210
285
|
composeVideoPromptText("a knight", { framingId: SHOT_SIZE, shotSize: SHOT_SIZE }),
|
|
211
|
-
).toBe(`a knight
|
|
286
|
+
).toBe(`a knight\n\n[style]:\n${getFramingPromptHint(SHOT_SIZE)}`)
|
|
212
287
|
})
|
|
213
288
|
|
|
214
289
|
it("emits BOTH clauses for two DIFFERENT ids of one catalog", () => {
|
|
215
290
|
// The case an alias table would have wrongly collapsed: `lightingId` is
|
|
216
291
|
// whole-catalog, so a time-of-day pick beside a lighting-style pick is two
|
|
217
|
-
// legitimate selections.
|
|
292
|
+
// legitimate selections. Both are scene-line clauses, so they share a line.
|
|
218
293
|
const out = composeVideoPromptText("a knight", {
|
|
219
294
|
lightingStyle: LIGHTING_STYLE,
|
|
220
295
|
lightingId: TIME_OF_DAY,
|
|
221
296
|
})
|
|
222
297
|
expect(out).toBe(
|
|
223
|
-
`a knight
|
|
298
|
+
`a knight\n\n[style]:\n` +
|
|
299
|
+
`${getLightingPromptHint(LIGHTING_STYLE)}. ${getLightingPromptHint(TIME_OF_DAY)}`,
|
|
224
300
|
)
|
|
225
301
|
})
|
|
302
|
+
|
|
303
|
+
it("emits NO header when every look clause deduped away", () => {
|
|
304
|
+
// The section's existence follows the clause COUNT after dedupe, not the
|
|
305
|
+
// caller's key count: two keys, one clause, one line — and a fold whose
|
|
306
|
+
// every look clause vanished would take the no-header path.
|
|
307
|
+
const out = composeVideoPromptText("a knight", {
|
|
308
|
+
framingId: SHOT_SIZE,
|
|
309
|
+
shotSize: SHOT_SIZE,
|
|
310
|
+
})!
|
|
311
|
+
expect(out.split("[style]:")).toHaveLength(2)
|
|
312
|
+
expect(composeVideoPromptText("a knight", { style: NO_SUCH_ID })).not.toContain("[style]")
|
|
313
|
+
})
|
|
226
314
|
})
|
|
227
315
|
|
|
228
316
|
/**
|
|
229
|
-
* ORDER TOTALITY — every video-surface dimension, folded in one call
|
|
317
|
+
* ORDER TOTALITY — every video-surface dimension, folded in one call, and the
|
|
318
|
+
* slot each one lands in.
|
|
230
319
|
*
|
|
231
320
|
* The fixture is keyed in `directionFieldsForSurface("video")` order and pinned
|
|
232
321
|
* against it, so adding, removing or reordering a video row fails HERE as well
|
|
233
322
|
* as in the registry test. Each id was chosen to render a clause distinct from
|
|
234
323
|
* every other row's, so the dedupe pass cannot mask a mis-ordering.
|
|
324
|
+
*
|
|
325
|
+
* The expectation is an INDEPENDENT recomputation off the registry (per-row
|
|
326
|
+
* clauses, classified by `family` + `FILM_STYLE_KEYS`, re-assembled by hand),
|
|
327
|
+
* not a second call through the composer — so a composer that slotted a row
|
|
328
|
+
* wrongly would have to be wrong in the registry to pass.
|
|
235
329
|
*/
|
|
236
330
|
const EVERY_VIDEO_DIMENSION: Record<string, string> = {
|
|
237
331
|
cameraMotion: "static",
|
|
@@ -284,18 +378,58 @@ describe("composeVideoPromptText — order totality over every video dimension",
|
|
|
284
378
|
}
|
|
285
379
|
})
|
|
286
380
|
|
|
287
|
-
it("folds all 35 dimensions
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
381
|
+
it("folds all 35 dimensions into body, film line and scene line", () => {
|
|
382
|
+
const filmKeys: readonly string[] = FILM_STYLE_KEYS
|
|
383
|
+
const bySlot: Record<"body" | "film" | "scene", string[]> = { body: [], film: [], scene: [] }
|
|
384
|
+
for (const row of directionFieldsForSurface("video")) {
|
|
385
|
+
const slot =
|
|
386
|
+
row.family === "motion" ? "body" : filmKeys.includes(row.key) ? "film" : "scene"
|
|
387
|
+
bySlot[slot].push(
|
|
388
|
+
...renderDirectionHints(
|
|
389
|
+
{ [row.key]: EVERY_VIDEO_DIMENSION[row.key] },
|
|
390
|
+
{ surface: "video", mode: VIDEO_HINT_MODE_DEFAULT },
|
|
391
|
+
),
|
|
392
|
+
)
|
|
393
|
+
}
|
|
394
|
+
const every = [...bySlot.body, ...bySlot.film, ...bySlot.scene]
|
|
395
|
+
expect(new Set(every).size, "fixture ids must render distinct clauses").toBe(every.length)
|
|
396
|
+
// Every slot has to be occupied or the assembly below proves less than it
|
|
397
|
+
// looks (an empty film line would collapse the section to one line).
|
|
398
|
+
for (const slot of ["body", "film", "scene"] as const) {
|
|
399
|
+
expect(bySlot[slot].length, slot).toBeGreaterThan(0)
|
|
400
|
+
}
|
|
401
|
+
|
|
297
402
|
expect(composeVideoPromptText("a knight", EVERY_VIDEO_DIMENSION)).toBe(
|
|
298
|
-
["a knight", ...
|
|
403
|
+
`${["a knight", ...bySlot.body].join(". ")}` +
|
|
404
|
+
`\n\n[style]:\n${bySlot.film.join(". ")}\n${bySlot.scene.join(". ")}`,
|
|
405
|
+
)
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
it("puts the five film rows on the film line and nothing else", () => {
|
|
409
|
+
expect(FILM_STYLE_KEYS).toEqual([
|
|
410
|
+
"cameraFormat",
|
|
411
|
+
"colorLook",
|
|
412
|
+
"style",
|
|
413
|
+
"era",
|
|
414
|
+
"cameraFormatId",
|
|
415
|
+
])
|
|
416
|
+
const filmLine = composeVideoPromptText("a knight", EVERY_VIDEO_DIMENSION)!
|
|
417
|
+
.split("\n\n[style]:\n")[1]!
|
|
418
|
+
.split("\n")[0]!
|
|
419
|
+
for (const key of FILM_STYLE_KEYS) {
|
|
420
|
+
if (!(key in EVERY_VIDEO_DIMENSION)) continue
|
|
421
|
+
const [clause] = renderDirectionHints(
|
|
422
|
+
{ [key]: EVERY_VIDEO_DIMENSION[key] },
|
|
423
|
+
{ surface: "video", mode: VIDEO_HINT_MODE_DEFAULT },
|
|
424
|
+
)
|
|
425
|
+
expect(filmLine, key).toContain(clause)
|
|
426
|
+
}
|
|
427
|
+
// …and a scene-line dimension never leaks onto it.
|
|
428
|
+
expect(filmLine).not.toContain(
|
|
429
|
+
renderDirectionHints(
|
|
430
|
+
{ setting: EVERY_VIDEO_DIMENSION.setting },
|
|
431
|
+
{ surface: "video", mode: VIDEO_HINT_MODE_DEFAULT },
|
|
432
|
+
)[0],
|
|
299
433
|
)
|
|
300
434
|
})
|
|
301
435
|
})
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
type PickerCatalog,
|
|
5
5
|
type PickerOption,
|
|
6
6
|
} from "../picker-catalogs.js"
|
|
7
|
+
import { STYLE_SECTION_HEADER } from "../prompt-style-section.js"
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* THE GUARD THAT MAKES "FOLD BEFORE THE REFERENCE RESOLVER" SAFE.
|
|
@@ -43,6 +44,14 @@ import {
|
|
|
43
44
|
* a dimension can join the direction channel at any time, and the guard must
|
|
44
45
|
* already hold when it does.
|
|
45
46
|
*
|
|
47
|
+
* A FIFTH pattern is the `[style]` section boundary. It runs in the other
|
|
48
|
+
* direction from the four above: those keep catalog text out of grammar the
|
|
49
|
+
* RESOLVER parses, this keeps catalog text out of grammar the ASSEMBLER emits.
|
|
50
|
+
* A catalog entry containing `[style]` would forge a section boundary inside a
|
|
51
|
+
* clause — enough to stop `buildImagePrompt`'s hybrid line-capitalizer early on
|
|
52
|
+
* an otherwise section-free prompt. Scanned case-insensitively for the same
|
|
53
|
+
* reason as `MENTION`: `buildMoodHint` lower-cases the labels it folds.
|
|
54
|
+
*
|
|
46
55
|
* A failure here is a CATALOG fix (reword the entry), never a fold-site change.
|
|
47
56
|
*/
|
|
48
57
|
|
|
@@ -61,12 +70,15 @@ const REF_ID_TOKEN = /\{ref:/i
|
|
|
61
70
|
const MENTION = /(?:^|[^A-Za-z0-9])@[a-z][a-z0-9-]*:\d+/i
|
|
62
71
|
/** The resolver's OUTPUT binding form (`video-reference-resolver.ts` emits `@${kind}_${n}`). */
|
|
63
72
|
const BINDING_FORM = /@(?:image|video|audio)_\d+/i
|
|
73
|
+
/** The `[style]` section boundary (`prompt-style-section.ts`'s header, colon-free so `[style]` alone trips). */
|
|
74
|
+
const STYLE_SECTION = /\[style\]/i
|
|
64
75
|
|
|
65
76
|
const FORBIDDEN: ReadonlyArray<{ name: string; re: RegExp }> = [
|
|
66
77
|
{ name: "reference slot token ({image:N} / {video:N} / {audio:N})", re: SLOT_TOKEN },
|
|
67
78
|
{ name: "id-addressed reference token ({ref:…})", re: REF_ID_TOKEN },
|
|
68
79
|
{ name: "character/named-image mention (@slug:N)", re: MENTION },
|
|
69
80
|
{ name: "reference binding form (@image_N / @video_N / @audio_N)", re: BINDING_FORM },
|
|
81
|
+
{ name: "style-section boundary ([style])", re: STYLE_SECTION },
|
|
70
82
|
]
|
|
71
83
|
|
|
72
84
|
/** Every option of a catalog, single-dim and multi-dim alike. */
|
|
@@ -110,4 +122,13 @@ describe("direction hint token safety", () => {
|
|
|
110
122
|
}
|
|
111
123
|
expect(offenders).toEqual([])
|
|
112
124
|
})
|
|
125
|
+
|
|
126
|
+
it("has a section header that is not itself reference grammar", () => {
|
|
127
|
+
// The header rides in the assembled prompt alongside catalog text, so it
|
|
128
|
+
// has to clear the same four resolver patterns the catalogs do — and it has
|
|
129
|
+
// to trip the fifth, or that guard is scanning for the wrong string.
|
|
130
|
+
for (const { name, re } of FORBIDDEN) {
|
|
131
|
+
expect(re.test(STYLE_SECTION_HEADER), name).toBe(re === STYLE_SECTION)
|
|
132
|
+
}
|
|
133
|
+
})
|
|
113
134
|
})
|