@trazum/core 1.86.0 → 2.0.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/README.md +3 -3
- package/dist/config-schema.d.ts +10 -10
- package/dist/config-schema.d.ts.map +1 -1
- package/dist/config-schema.js +90 -36
- package/dist/config-schema.js.map +1 -1
- package/dist/optimize.d.ts.map +1 -1
- package/dist/optimize.js +16 -1
- package/dist/optimize.js.map +1 -1
- package/dist/phrases.d.ts.map +1 -1
- package/dist/phrases.js +36 -0
- package/dist/phrases.js.map +1 -1
- package/dist/receipt.d.ts +5 -5
- package/dist/receipt.d.ts.map +1 -1
- package/dist/receipt.js +37 -18
- package/dist/receipt.js.map +1 -1
- package/dist/segment.d.ts.map +1 -1
- package/dist/segment.js +95 -1
- package/dist/segment.js.map +1 -1
- package/dist/structure.d.ts +27 -0
- package/dist/structure.d.ts.map +1 -1
- package/dist/structure.js +277 -9
- package/dist/structure.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/config-schema.ts +100 -44
- package/src/optimize.ts +16 -1
- package/src/phrases.ts +36 -0
- package/src/receipt.ts +40 -18
- package/src/segment.ts +95 -1
- package/src/structure.ts +275 -9
- package/src/types.ts +2 -0
package/dist/receipt.js
CHANGED
|
@@ -1,26 +1,45 @@
|
|
|
1
1
|
import { PROVIDER_REVIEWED, modelFrom } from './pricing.js';
|
|
2
2
|
import { UNLABELLED } from './usage.js';
|
|
3
3
|
/**
|
|
4
|
-
* The fields that leave this machine,
|
|
4
|
+
* The fields that leave this machine, keyed by the interface they describe.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* added to `ReceiptLine` and not added here
|
|
8
|
-
*
|
|
6
|
+
* `Record<keyof ReceiptLine, true>` is what holds the two together: a field
|
|
7
|
+
* added to `ReceiptLine` and not added here is a **compile error**, and so is
|
|
8
|
+
* an entry here that `ReceiptLine` does not have. A whitelist maintained in two
|
|
9
|
+
* places is a whitelist with a hole in it.
|
|
10
|
+
*
|
|
11
|
+
* **This used to say the redaction guard held that, and it did not.** The guard
|
|
12
|
+
* reads the emitted lines and checks every key it finds against this list,
|
|
13
|
+
* which catches an emitter that emits an unlisted field — a real property, kept
|
|
14
|
+
* below. It cannot catch a field added to the *type*: a plant added
|
|
15
|
+
* `operator?: string` to `ReceiptLine`, emitted it on no line the fixture
|
|
16
|
+
* built, and all twelve checks passed. A field populated in one branch is
|
|
17
|
+
* exactly the shape a leak takes, and the published whitelist customers read to
|
|
18
|
+
* see what leaves their machine would have been wrong while its own guard said
|
|
19
|
+
* otherwise.
|
|
20
|
+
*/
|
|
21
|
+
const RECEIPT_LINE_FIELD_MAP = {
|
|
22
|
+
label: true,
|
|
23
|
+
model: true,
|
|
24
|
+
calls: true,
|
|
25
|
+
inputTokens: true,
|
|
26
|
+
cacheReadTokens: true,
|
|
27
|
+
cacheWriteTokens: true,
|
|
28
|
+
cacheWrite5mTokens: true,
|
|
29
|
+
cacheWrite1hTokens: true,
|
|
30
|
+
outputTokens: true,
|
|
31
|
+
usd: true,
|
|
32
|
+
money: true,
|
|
33
|
+
pricing: true,
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* The same list, published.
|
|
37
|
+
*
|
|
38
|
+
* Derived from the map rather than written again, because writing it twice is
|
|
39
|
+
* the fault this exists to prevent. Key order is insertion order, so the array
|
|
40
|
+
* a consumer reads is unchanged.
|
|
9
41
|
*/
|
|
10
|
-
export const RECEIPT_LINE_FIELDS = Object.freeze(
|
|
11
|
-
'label',
|
|
12
|
-
'model',
|
|
13
|
-
'calls',
|
|
14
|
-
'inputTokens',
|
|
15
|
-
'cacheReadTokens',
|
|
16
|
-
'cacheWriteTokens',
|
|
17
|
-
'cacheWrite5mTokens',
|
|
18
|
-
'cacheWrite1hTokens',
|
|
19
|
-
'outputTokens',
|
|
20
|
-
'usd',
|
|
21
|
-
'money',
|
|
22
|
-
'pricing',
|
|
23
|
-
]);
|
|
42
|
+
export const RECEIPT_LINE_FIELDS = Object.freeze(Object.keys(RECEIPT_LINE_FIELD_MAP));
|
|
24
43
|
const lineFrom = (label, model, breakdown, catalogue) => {
|
|
25
44
|
/*
|
|
26
45
|
* Every slice reaching here is priced: `byLabelAndModel` holds only what the
|
package/dist/receipt.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"receipt.js","sourceRoot":"","sources":["../src/receipt.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE5D,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AA2NxC
|
|
1
|
+
{"version":3,"file":"receipt.js","sourceRoot":"","sources":["../src/receipt.ts"],"names":[],"mappings":"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"}
|
package/dist/segment.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"segment.d.ts","sourceRoot":"","sources":["../src/segment.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAkB,OAAO,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"segment.d.ts","sourceRoot":"","sources":["../src/segment.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAkB,OAAO,EAAE,MAAM,YAAY,CAAC;AA2I1D,wEAAwE;AACxE,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,EAAE,CAqE/C;AAED,gDAAgD;AAChD,wBAAgB,IAAI,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAEhD;AAED,iEAAiE;AACjE,wBAAgB,cAAc,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,EAAE,CAE5D"}
|
package/dist/segment.js
CHANGED
|
@@ -2,8 +2,102 @@
|
|
|
2
2
|
const PATTERNS = [
|
|
3
3
|
// Fenced code blocks with ``` or ~~~ (unclosed blocks run to the end).
|
|
4
4
|
{ kind: 'fenced-code', regex: /(?:```|~~~)[\s\S]*?(?:```|~~~|$)/g },
|
|
5
|
-
|
|
5
|
+
/*
|
|
6
|
+
Indented code blocks, which this file used to leave to the rules on the
|
|
7
|
+
grounds that they are *"ambiguous in markdown"*. The ambiguity is real. The
|
|
8
|
+
consequence of acting on it was not proportionate to it.
|
|
9
|
+
|
|
10
|
+
**What the rules did to four lines of indented code**, at the aggressive
|
|
11
|
+
level, every one measured rather than imagined:
|
|
12
|
+
|
|
13
|
+
const label = "please keep"; -> Const label = " keep";
|
|
14
|
+
def run(x): -> Def run(x):
|
|
15
|
+
SELECT … WHERE note = 'please refund'; -> … = ' refund';
|
|
16
|
+
{"reason": "please cancel"} -> {"reason": " cancel"}
|
|
17
|
+
|
|
18
|
+
Three separate failures at once. The indentation goes, which by itself
|
|
19
|
+
makes the Python a syntax error. Keywords get sentence-capitalised, which
|
|
20
|
+
makes the rest of them syntax errors. And **string literals are edited** —
|
|
21
|
+
that SQL now matches a different value and that payload carries a different
|
|
22
|
+
reason. The report says tokens were saved.
|
|
23
|
+
|
|
24
|
+
The blank line before the block is what makes this checkable rather than a
|
|
25
|
+
guess: CommonMark says an indented code block cannot interrupt a paragraph,
|
|
26
|
+
so a run of indented lines after a blank line is code by the specification
|
|
27
|
+
rather than by anybody's judgement here.
|
|
28
|
+
|
|
29
|
+
**What it still gets wrong, stated rather than discovered later.** Content
|
|
30
|
+
indented four or more spaces inside a deeply nested list is continuation,
|
|
31
|
+
not code, and this protects it. That costs unsaved tokens in a rare shape.
|
|
32
|
+
Not protecting code costs a broken prompt in a common one, and those are not
|
|
33
|
+
the same kind of wrong.
|
|
34
|
+
|
|
35
|
+
Every quantifier is bounded or consumes a whole line exactly once, for the
|
|
36
|
+
reason the email mask below now carries in full.
|
|
37
|
+
*/
|
|
38
|
+
{
|
|
39
|
+
kind: 'indented-code',
|
|
40
|
+
/*
|
|
41
|
+
**The lookbehind requires real content before the blank line, and the
|
|
42
|
+
fuzzer is why.** Its first version asked only for a blank line, which a
|
|
43
|
+
document *beginning* with one satisfies — and `optimize` ends with a
|
|
44
|
+
`.trim()` on the whole reassembled string, outside every mask, which then
|
|
45
|
+
removes the leading newlines and the first line's indentation. The block
|
|
46
|
+
stopped being a block, the second pass no longer protected it, and 54 of
|
|
47
|
+
1,500 corpus inputs failed the idempotence property.
|
|
48
|
+
|
|
49
|
+
The trim is fixed at its source now — `optimize` trims the *masked* string,
|
|
50
|
+
where a protected span is a placeholder nothing can edit — so the mask can
|
|
51
|
+
also claim a block at the very start of a document. It has to: at the
|
|
52
|
+
aggressive level a rule can delete the sentence before the blank line, and
|
|
53
|
+
a block that was protected on the first pass has to still be protected on
|
|
54
|
+
the second or the output never settles.
|
|
55
|
+
*/
|
|
56
|
+
regex: /(?<=\S[ \t]*\n[ \t]*\n)(?:(?: {4,}|\t)[^\n]*(?:\n|$))+/g,
|
|
57
|
+
},
|
|
6
58
|
{ kind: 'url', regex: /\b(?:https?|ftp):\/\/[^\s<>"')\]]+/g, trimTrailing: /[.,;:!?]+$/ },
|
|
59
|
+
/*
|
|
60
|
+
Email addresses, for the reason at the top of this file.
|
|
61
|
+
|
|
62
|
+
**Five of ten realistic addresses came out corrupted before this existed.**
|
|
63
|
+
`please@example.com` became `@example.com`, and so did `thanks@`,
|
|
64
|
+
`basically@`, `essentially.ops@` and `very.important@`: the politeness,
|
|
65
|
+
filler and intensifier rules match the local part as ordinary prose and cut
|
|
66
|
+
it out. What is left is not a wrong address, it is not an address — and the
|
|
67
|
+
report says tokens were saved.
|
|
68
|
+
|
|
69
|
+
That is the failure this module's own first paragraph names. A code block, a
|
|
70
|
+
URL and a placeholder were on the list; the thing every support prompt in
|
|
71
|
+
the world carries was not. `support@` and `no-reply@` survived, and
|
|
72
|
+
`por.favor@ejemplo.es` survived only because the Spanish politeness entry is
|
|
73
|
+
written with a space — luck, not design, which is what made it invisible.
|
|
74
|
+
|
|
75
|
+
After the URL pattern so a `mailto:` link is claimed as a URL first, and
|
|
76
|
+
trailing punctuation is trimmed for the same reason it is there: the full
|
|
77
|
+
stop in "write to a@b.com." belongs to the sentence.
|
|
78
|
+
*/
|
|
79
|
+
{
|
|
80
|
+
kind: 'email',
|
|
81
|
+
/*
|
|
82
|
+
**Every quantifier here is bounded, and the first version of this was not.**
|
|
83
|
+
The local part accepts `.`, so on a long dotted run with no `@` in it —
|
|
84
|
+
a dotted run behind a scheme, which is a fixture in `security.test.js`
|
|
85
|
+
for exactly this reason — an unbounded `+` matches the whole run from every starting
|
|
86
|
+
position and then fails. Quadratic: **897ms on 40,008 characters**, and
|
|
87
|
+
`optimize` runs this alongside every other pattern, which took the total
|
|
88
|
+
past the 5-second cliff detector on a CI runner while passing on a faster
|
|
89
|
+
machine. That test's own note had already recorded the lesson from the
|
|
90
|
+
last time — *"quadratic until the label quantifiers were bounded"* — and
|
|
91
|
+
this pattern arrived a week later without them.
|
|
92
|
+
|
|
93
|
+
The bounds are RFC 5321's, not invented: 64 octets for a local part, 63
|
|
94
|
+
for a domain label. The TLD's 24 is generous against the longest real one
|
|
95
|
+
(`.travelersinsurance`, 19). Bounded, the same input takes **8ms**, and no
|
|
96
|
+
address in the corpus is lost.
|
|
97
|
+
*/
|
|
98
|
+
regex: /\b[A-Za-z0-9._%+-]{1,64}@[A-Za-z0-9-]{1,63}(?:\.[A-Za-z0-9-]{1,63})*\.[A-Za-z]{2,24}/g,
|
|
99
|
+
trimTrailing: /[.,;:!?]+$/,
|
|
100
|
+
},
|
|
7
101
|
// Template placeholders: {{var}}, {var}, ${var}, {% tag %}, <<VAR>>, %(var)s
|
|
8
102
|
{
|
|
9
103
|
kind: 'placeholder',
|
package/dist/segment.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"segment.js","sourceRoot":"","sources":["../src/segment.ts"],"names":[],"mappings":"AAqBA,gEAAgE;AAChE,MAAM,QAAQ,GAAiB;IAC7B,uEAAuE;IACvE,EAAE,IAAI,EAAE,aAAa,EAAE,KAAK,EAAE,mCAAmC,EAAE;IACnE,
|
|
1
|
+
{"version":3,"file":"segment.js","sourceRoot":"","sources":["../src/segment.ts"],"names":[],"mappings":"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"}
|
package/dist/structure.d.ts
CHANGED
|
@@ -67,6 +67,33 @@ export interface ExampleAnalysis {
|
|
|
67
67
|
/** Tokens held by examples that repeat an earlier one. */
|
|
68
68
|
redundantTokens: number;
|
|
69
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* **A shape this deliberately does not detect, and the reason is the point.**
|
|
72
|
+
*
|
|
73
|
+
* Inline mappings are everywhere — `1. "package never arrived" -> shipping`,
|
|
74
|
+
* `- "Acme raised a round" => Acme` — and on the probes that found the two
|
|
75
|
+
* faults above they are the remaining silent case. They are not added, because
|
|
76
|
+
* the only thing separating a demonstration from an instruction here is
|
|
77
|
+
* judgement: `if the ticket is about delivery -> use the shipping category` is
|
|
78
|
+
* the same line shape and is a rule, not an example.
|
|
79
|
+
*
|
|
80
|
+
* A detector that got that wrong would not merely report a wrong number.
|
|
81
|
+
* `prune` *removes* examples, so a rule misread as an example becomes a
|
|
82
|
+
* proposal to delete an instruction — and it would arrive with the same
|
|
83
|
+
* confidence as a correct one.
|
|
84
|
+
*
|
|
85
|
+
* The tempting narrowing is to require the left side to be quoted. It was
|
|
86
|
+
* considered and refused: both probes are quoted **because the author of the
|
|
87
|
+
* probes wrote them that way**, and a threshold fitted to one's own fixture is
|
|
88
|
+
* a threshold that has been measured against nothing. What would settle it is
|
|
89
|
+
* real prompts using the shape, not a rule invented here.
|
|
90
|
+
*
|
|
91
|
+
* Until then the honest behaviour is the one `prune` now has: say what was
|
|
92
|
+
* looked for, so a prompt this cannot read reports that it was not seen rather
|
|
93
|
+
* than that it does not exist. That sentence used to read "this prompt has
|
|
94
|
+
* fewer than two few-shot examples", which was a confident false statement
|
|
95
|
+
* about a prompt with four of them.
|
|
96
|
+
*/
|
|
70
97
|
/** Splits the prompt into labelled example blocks. */
|
|
71
98
|
export declare function findExamples(prompt: string, count: TokenCounter): ExampleBlock[];
|
|
72
99
|
/**
|
package/dist/structure.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"structure.d.ts","sourceRoot":"","sources":["../src/structure.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AACjF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C;;;;;;;;;;;GAWG;AAMH;;;GAGG;AACH,MAAM,MAAM,iBAAiB,GAAG,mBAAmB,CAAC;AAEpD,MAAM,WAAW,iBAAiB;IAChC,sDAAsD;IACtD,KAAK,EAAE,oBAAoB,CAAC;IAC5B,yDAAyD;IACzD,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,iBAAiB,CAAC;IACxB,CAAC,EAAE,iBAAiB,CAAC;IACrB,CAAC,EAAE,iBAAiB,CAAC;CACtB;
|
|
1
|
+
{"version":3,"file":"structure.d.ts","sourceRoot":"","sources":["../src/structure.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AACjF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C;;;;;;;;;;;GAWG;AAMH;;;GAGG;AACH,MAAM,MAAM,iBAAiB,GAAG,mBAAmB,CAAC;AAEpD,MAAM,WAAW,iBAAiB;IAChC,sDAAsD;IACtD,KAAK,EAAE,oBAAoB,CAAC;IAC5B,yDAAyD;IACzD,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,iBAAiB,CAAC;IACxB,CAAC,EAAE,iBAAiB,CAAC;IACrB,CAAC,EAAE,iBAAiB,CAAC;CACtB;AA6JD;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,EAAE,CA6BlE;AAsHD;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,QAG9B,CAAC;AAEF,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,8DAA8D;IAC9D,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,SAAS,EAAE,gBAAgB,EAAE,CAAC;IAC9B,0DAA0D;IAC1D,eAAe,EAAE,MAAM,CAAC;CACzB;AA0HD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,sDAAsD;AACtD,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,YAAY,EAAE,CAchF;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,eAAe,CAyBpF;AAMD;;;;;;;;;GASG;AACH,MAAM,WAAW,cAAc;IAC7B,iDAAiD;IACjD,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,qEAAqE;IACrE,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,4DAA4D;IAC5D,cAAc,EAAE,MAAM,CAAC;CACxB;AAuJD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,cAAc,GAAG,IAAI,CAuC7F;AAUD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,WAAW,aAAa;IAC5B,mDAAmD;IACnD,MAAM,EAAE,MAAM,CAAC;IACf,uDAAuD;IACvD,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,kFAAkF;IAClF,MAAM,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,GAAG,EAAE,MAAM,CAAC;CACb;AAqBD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,aAAa,GAAG,IAAI,CA4C3F"}
|
package/dist/structure.js
CHANGED
|
@@ -9,6 +9,27 @@ import { jaccard, normalizeForCompare } from './similarity.js';
|
|
|
9
9
|
* merely mentions a language, which is most translation prompts.
|
|
10
10
|
*/
|
|
11
11
|
const RESPOND = String.raw `(?:respond|reply|answer|write|speak|output|responde|contesta|responder|escribe|redacta)`;
|
|
12
|
+
/**
|
|
13
|
+
* How a prompt refers to the reader having produced their message.
|
|
14
|
+
*
|
|
15
|
+
* `used|wrote|speaks` missed on a tense: *"respond in the same language the
|
|
16
|
+
* user writes in"* — present, which is how a standing instruction is normally
|
|
17
|
+
* written — was invisible, so a prompt pinning English and mirroring the user
|
|
18
|
+
* in the same breath reported no conflict at all.
|
|
19
|
+
*/
|
|
20
|
+
const SPOKE = String.raw `(?:used?|uses|wrote|writes|writing|speaks|spoke|sent|asked)`;
|
|
21
|
+
/** Nouns that mean *the thing the model produces*, as opposed to any other text. */
|
|
22
|
+
const THE_ANSWER = String.raw `(?:answers?|responses?|repl(?:y|ies)|explanations?|respuestas?)`;
|
|
23
|
+
/**
|
|
24
|
+
* A numeric ceiling on length, without the unit.
|
|
25
|
+
*
|
|
26
|
+
* Kept separate because it is only ever safe next to an anchor: *"the title
|
|
27
|
+
* field takes at most five words"* is the same phrase about a form field, and
|
|
28
|
+
* reading it as an instruction about the answer is how a widening turns a
|
|
29
|
+
* detector into a nuisance.
|
|
30
|
+
*/
|
|
31
|
+
const REQUIRE_SHORT = String.raw `keep\s+(?:it|them|${THE_ANSWER})\s+(?:short|brief|concise)`;
|
|
32
|
+
const AT_MOST = String.raw `(?:at\s+most|no\s+more\s+than|under|within|máximo\s+de?)\s+(?:one|two|three|four|five|\d+)\s+(?:sentences?|words?|frases?|palabras?)`;
|
|
12
33
|
const AXES = [
|
|
13
34
|
{
|
|
14
35
|
axis: 'response-language',
|
|
@@ -23,7 +44,7 @@ const AXES = [
|
|
|
23
44
|
// The qualifier slot ("own", "native", "preferred") is what makes
|
|
24
45
|
// this survive real prompts: "the customer's own language" is at
|
|
25
46
|
// least as common as the bare form.
|
|
26
|
-
String.raw `\bin\s+the\s+(?:same\s+)?language\s+(?:of|as|the\s+\w+\s+(?:used|wrote|speaks))|\bin\s+(?:the\s+(?:user|customer|client)'?s?|their)\s+(?:own\s+|native\s+|preferred\s+)?language\b|\bmatch\s+the\s+language\b|\ben\s+(?:el\s+)?(?:mismo\s+)?idioma\s+(?:del?\s+|en\s+que\s+)?(?:usuario|cliente|consulta|mensaje|escrib)`, 'i'),
|
|
47
|
+
String.raw `\bin\s+the\s+(?:same\s+)?language\s+(?:of|as|the\s+\w+\s+(?:used|wrote|speaks))|\bin\s+(?:the\s+(?:user|customer|client)'?s?|their)\s+(?:own\s+|native\s+|preferred\s+)?language\b|\bmatch\s+the\s+language\b|\b${RESPOND}\b[^.!?\n]{0,40}?\bin\s+(?:the\s+(?:same\s+)?|whatever\s+|whichever\s+)language\s+(?:of|as|that\s+|the\s+\w+\s+)?\w*\s*${SPOKE}|\ben\s+(?:el\s+)?(?:mismo\s+)?idioma\s+(?:del?\s+|en\s+que\s+)?(?:usuario|cliente|consulta|mensaje|escrib)`, 'i'),
|
|
27
48
|
],
|
|
28
49
|
],
|
|
29
50
|
},
|
|
@@ -49,11 +70,11 @@ const AXES = [
|
|
|
49
70
|
values: [
|
|
50
71
|
[
|
|
51
72
|
'length-brief',
|
|
52
|
-
new RegExp(String.raw `\b(?:be\s+(?:brief|concise|succinct|terse)|keep\s+it\s+(?:short|brief)|as\s+short\s+as\s+possible|in\s+(?:one|a\s+single)\s+sentence|s[ée]\s+(?:breve|conciso)|de\s+forma\s+(?:breve|concisa)|brevemente)\b`, 'i'),
|
|
73
|
+
new RegExp(String.raw `\b(?:be\s+(?:brief|concise|succinct|terse)|keep\s+it\s+(?:short|brief)|as\s+short\s+as\s+possible|${REQUIRE_SHORT}|\b${RESPOND}\b[^.!?\n]{0,40}?${AT_MOST}|in\s+(?:one|a\s+single)\s+sentence|s[ée]\s+(?:breve|conciso)|de\s+forma\s+(?:breve|concisa)|brevemente)\b`, 'i'),
|
|
53
74
|
],
|
|
54
75
|
[
|
|
55
76
|
'length-detailed',
|
|
56
|
-
new RegExp(String.raw `\b(?:be\s+(?:detailed|comprehensive|thorough|exhaustive)|in\s+(?:great\s+)?depth|as\s+much\s+detail\s+as\s+possible|s[ée]\s+(?:exhaustivo|detallado)|de\s+forma\s+(?:detallada|exhaustiva)|con\s+todo\s+detalle|detalladamente)\b`, 'i'),
|
|
77
|
+
new RegExp(String.raw `\b(?:be\s+(?:detailed|comprehensive|thorough|exhaustive)|in\s+(?:great\s+)?depth|(?:detailed|comprehensive|thorough|exhaustive|in-depth)[^.!?\n]{0,30}?\b${THE_ANSWER}\b|as\s+much\s+detail\s+as\s+possible|s[ée]\s+(?:exhaustivo|detallado)|de\s+forma\s+(?:detallada|exhaustiva)|con\s+todo\s+detalle|detalladamente)\b`, 'i'),
|
|
57
78
|
],
|
|
58
79
|
],
|
|
59
80
|
},
|
|
@@ -62,11 +83,11 @@ const AXES = [
|
|
|
62
83
|
values: [
|
|
63
84
|
[
|
|
64
85
|
'reasoning-shown',
|
|
65
|
-
new RegExp(String.raw `\b(?:explain\s+your\s+(?:reasoning|answer|thinking)|show\s+your\s+(?:work|reasoning|thinking)|think\s+step[\s-]by[\s-]step|justify\s+your\s+answer|explica\s+tu\s+razonamiento|razona\s+paso\s+a\s+paso|justifica\s+tu\s+respuesta)\b`, 'i'),
|
|
86
|
+
new RegExp(String.raw `\b(?:explain\s+your\s+(?:reasoning|answer|thinking)|show\s+your\s+(?:work|reasoning|thinking)|show\s+your\s+chain\s+of\s+thought|walk\s+(?:me\s+)?through\s+your\s+(?:reasoning|thinking|logic)|think\s+step[\s-]by[\s-]step|justify\s+your\s+answer|explica\s+tu\s+razonamiento|razona\s+paso\s+a\s+paso|justifica\s+tu\s+respuesta)\b`, 'i'),
|
|
66
87
|
],
|
|
67
88
|
[
|
|
68
89
|
'reasoning-hidden',
|
|
69
|
-
new RegExp(String.raw `\b(?:no\s+(?:explanation|commentary|preamble)|without\s+(?:explanation|commentary)|do\s+not\s+explain|don'?t\s+explain|no\s+expliques|sin\s+(?:explicaciones|comentarios)|sin\s+preámbulo)\b`, 'i'),
|
|
90
|
+
new RegExp(String.raw `\b(?:no\s+(?:explanation|commentary|preamble)|without\s+(?:explanation|commentary|justification|reasoning)|no\s+justification|do\s+not\s+explain|don'?t\s+explain|no\s+expliques|sin\s+(?:explicaciones|comentarios)|sin\s+preámbulo)\b`, 'i'),
|
|
70
91
|
],
|
|
71
92
|
],
|
|
72
93
|
},
|
|
@@ -168,10 +189,79 @@ const LABEL_SUFFIX = String.raw `[ \t]{0,4}\d{0,6}[ \t]{0,4}[:.)]`;
|
|
|
168
189
|
* indistinguishable from ordinary prose, and guessing would make the advisory
|
|
169
190
|
* fire on prompts containing no examples at all.
|
|
170
191
|
*/
|
|
192
|
+
/**
|
|
193
|
+
* The label on the side that **asks**, in the wordings prompts actually use.
|
|
194
|
+
*
|
|
195
|
+
* **This list was three entries long and it made six analyses blind.** A
|
|
196
|
+
* support prompt labelled `Customer:` / `Agent:` — the single most common
|
|
197
|
+
* shape there is — found zero examples, so `prune` had nothing to cut,
|
|
198
|
+
* `profile` reported the examples as costing nothing, and the redundant-example
|
|
199
|
+
* advisory never fired. Measured across fourteen labellings real prompts use,
|
|
200
|
+
* nine found nothing: `Customer`, `Human`, `Question`, `Client`, `Prompt`,
|
|
201
|
+
* `Query`, `Cliente`, `Pregunta`, `REQUEST`. `Human:` is Anthropic's own
|
|
202
|
+
* historical convention and `Q:` worked while `Question:` did not, which is
|
|
203
|
+
* the arbitrariness that gives the fault away.
|
|
204
|
+
*
|
|
205
|
+
* The failure was silent, which is what made it survive: an unrecognised label
|
|
206
|
+
* produces no examples, and no examples produces no finding — the same shape
|
|
207
|
+
* as a gate that passes because it is checking nothing.
|
|
208
|
+
*
|
|
209
|
+
* Only openers belong here. `Answer:` and `Assistant:` close a turn, and
|
|
210
|
+
* splitting on those would cut every example between its question and its
|
|
211
|
+
* answer and then compare the halves.
|
|
212
|
+
*/
|
|
213
|
+
const ASKER_LABELS = [
|
|
214
|
+
'user',
|
|
215
|
+
'usuario',
|
|
216
|
+
'human',
|
|
217
|
+
'humano',
|
|
218
|
+
'customer',
|
|
219
|
+
'cliente',
|
|
220
|
+
'client',
|
|
221
|
+
'q',
|
|
222
|
+
'question',
|
|
223
|
+
'pregunta',
|
|
224
|
+
'query',
|
|
225
|
+
'consulta',
|
|
226
|
+
'request',
|
|
227
|
+
'petición',
|
|
228
|
+
'peticion',
|
|
229
|
+
'prompt',
|
|
230
|
+
];
|
|
231
|
+
/**
|
|
232
|
+
* The label on the side that **answers**.
|
|
233
|
+
*
|
|
234
|
+
* Never a tier: these appear inside an example rather than opening one. They
|
|
235
|
+
* are here so `EXAMPLE_FIELD_LINE` can be built from the same two lists the
|
|
236
|
+
* splitter uses, which is what stops the pair drifting apart again — the field
|
|
237
|
+
* detector already knew `question` and `answer` while the splitter did not,
|
|
238
|
+
* and that disagreement is precisely how `Q:` came to work and `Question:` not.
|
|
239
|
+
*/
|
|
240
|
+
const ANSWERER_LABELS = [
|
|
241
|
+
'assistant',
|
|
242
|
+
'asistente',
|
|
243
|
+
'a',
|
|
244
|
+
'answer',
|
|
245
|
+
'respuesta',
|
|
246
|
+
'output',
|
|
247
|
+
'salida',
|
|
248
|
+
'response',
|
|
249
|
+
'reply',
|
|
250
|
+
'agent',
|
|
251
|
+
'agente',
|
|
252
|
+
'support',
|
|
253
|
+
'soporte',
|
|
254
|
+
'bot',
|
|
255
|
+
'result',
|
|
256
|
+
'resultado',
|
|
257
|
+
];
|
|
258
|
+
/** Labels that name an example as a whole, rather than one side of it. */
|
|
259
|
+
const BLOCK_LABELS = ['example', 'ejemplo', 'input', 'entrada'];
|
|
260
|
+
const anyOf = (labels) => labels.join('|');
|
|
171
261
|
const EXAMPLE_HEADER_TIERS = [
|
|
172
262
|
new RegExp(`${LABEL_PREFIX}(?:example|ejemplo)${LABEL_SUFFIX}`, 'i'),
|
|
173
263
|
new RegExp(`${LABEL_PREFIX}(?:input|entrada)${LABEL_SUFFIX}`, 'i'),
|
|
174
|
-
new RegExp(`${LABEL_PREFIX}(
|
|
264
|
+
new RegExp(`${LABEL_PREFIX}(?:${anyOf(ASKER_LABELS)})${LABEL_SUFFIX}`, 'i'),
|
|
175
265
|
];
|
|
176
266
|
/**
|
|
177
267
|
* A labelled field inside a few-shot example — `Input:`, `Output:`, `A:`…
|
|
@@ -182,7 +272,7 @@ const EXAMPLE_HEADER_TIERS = [
|
|
|
182
272
|
* those lines leaves an example with no output at all — worse than the
|
|
183
273
|
* repetition it removed.
|
|
184
274
|
*/
|
|
185
|
-
export const EXAMPLE_FIELD_LINE = new RegExp(`${LABEL_PREFIX}(
|
|
275
|
+
export const EXAMPLE_FIELD_LINE = new RegExp(`${LABEL_PREFIX}(?:${anyOf([...BLOCK_LABELS, ...ASKER_LABELS, ...ANSWERER_LABELS])})${LABEL_SUFFIX}`, 'i');
|
|
186
276
|
/**
|
|
187
277
|
* Similarity at which two examples are treated as repeating each other.
|
|
188
278
|
*
|
|
@@ -201,6 +291,30 @@ export const EXAMPLE_FIELD_LINE = new RegExp(`${LABEL_PREFIX}(?:input|output|ent
|
|
|
201
291
|
* "arrived fast" teach the same thing needs a model, not a word-set overlap;
|
|
202
292
|
* that belongs to the optional LLM pass, not here.
|
|
203
293
|
*/
|
|
294
|
+
/**
|
|
295
|
+
* How alike two examples must be before one is called redundant.
|
|
296
|
+
*
|
|
297
|
+
* **Do not lower this hoping to catch paraphrases: it was measured and it does
|
|
298
|
+
* not work.** Four support examples whose answers were *"Let me look that up
|
|
299
|
+
* for you. Could you share your order number?"*, *"I can check that for you.
|
|
300
|
+
* What's your order number?"*, *"Happy to help. May I have your order
|
|
301
|
+
* number?"* and *"Of course. What is your order number?"* are one example
|
|
302
|
+
* wearing four coats — any reader says so instantly. Their word overlap is
|
|
303
|
+
* 0.29–0.43, and comparing only the answers makes it **worse**, 0.21–0.39,
|
|
304
|
+
* because the shared content is three words (`your`, `order`, `number`) while
|
|
305
|
+
* the openers are all different. There is no threshold that separates those
|
|
306
|
+
* from genuinely different examples: at 0.4 this fires on every prompt whose
|
|
307
|
+
* examples share a domain vocabulary.
|
|
308
|
+
*
|
|
309
|
+
* That similarity is a semantic judgement, and this repository has a place for
|
|
310
|
+
* those — `trazum semantic`, which says what it costs and asks first. What
|
|
311
|
+
* word overlap can honestly find is a paragraph somebody pasted twice and
|
|
312
|
+
* edited lightly, which is what 0.7 is for.
|
|
313
|
+
*
|
|
314
|
+
* So the deterministic half reports **what the examples cost**, which is a
|
|
315
|
+
* fact, and leaves *whether one should go* to `prune`, which answers it by
|
|
316
|
+
* running the cases rather than by counting words.
|
|
317
|
+
*/
|
|
204
318
|
const EXAMPLE_SIMILARITY = 0.7;
|
|
205
319
|
/** Splits the prompt on one header style. */
|
|
206
320
|
function splitOnHeader(lines, header) {
|
|
@@ -245,9 +359,67 @@ function trimToExample(block) {
|
|
|
245
359
|
}
|
|
246
360
|
return kept.join('\n\n').trim();
|
|
247
361
|
}
|
|
362
|
+
/**
|
|
363
|
+
* Examples delimited by an `<example>` tag rather than by a label.
|
|
364
|
+
*
|
|
365
|
+
* **This is Anthropic's own documented convention for few-shot prompting**, and
|
|
366
|
+
* the product whose headline model is Claude could not see it: a code-review
|
|
367
|
+
* prompt wrapping three demonstrations in `<example>` found zero examples, and
|
|
368
|
+
* the same six analyses went quiet that `Customer:` had silenced. A shape a
|
|
369
|
+
* provider tells people to use is not an edge case.
|
|
370
|
+
*
|
|
371
|
+
* Tried before the label tiers because it is the least ambiguous thing in this
|
|
372
|
+
* file. A label is a word that might be prose; `<example>` is a delimiter, so
|
|
373
|
+
* there is no threshold and no judgement — either the tag is there or it is not.
|
|
374
|
+
*
|
|
375
|
+
* Only `example` and `examples`, which are what the documentation names.
|
|
376
|
+
* `<sample>` and `<demonstration>` appear in the wild and are not added: the
|
|
377
|
+
* list would then be a guess about what people might write, and this splitter
|
|
378
|
+
* has already been burnt once by a vocabulary somebody thought was wide enough.
|
|
379
|
+
* A tag arriving here should arrive with a prompt that uses it.
|
|
380
|
+
*
|
|
381
|
+
* A tag inside a fenced block is documentation about the tag rather than a use
|
|
382
|
+
* of it — a prompt explaining how to write examples is not a prompt containing
|
|
383
|
+
* them — so fenced regions are removed before matching.
|
|
384
|
+
*/
|
|
385
|
+
function xmlExamples(prompt) {
|
|
386
|
+
const outsideFences = prompt.replace(/^([ \t]{0,3})(`{3,}|~{3,})[\s\S]*?^\1\2[ \t]*$/gm, '');
|
|
387
|
+
const tag = /<example(?:\s[^>]*)?>([\s\S]*?)<\/example>/gi;
|
|
388
|
+
return [...outsideFences.matchAll(tag)].map(([, body]) => (body ?? '').trim()).filter(Boolean);
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* **A shape this deliberately does not detect, and the reason is the point.**
|
|
392
|
+
*
|
|
393
|
+
* Inline mappings are everywhere — `1. "package never arrived" -> shipping`,
|
|
394
|
+
* `- "Acme raised a round" => Acme` — and on the probes that found the two
|
|
395
|
+
* faults above they are the remaining silent case. They are not added, because
|
|
396
|
+
* the only thing separating a demonstration from an instruction here is
|
|
397
|
+
* judgement: `if the ticket is about delivery -> use the shipping category` is
|
|
398
|
+
* the same line shape and is a rule, not an example.
|
|
399
|
+
*
|
|
400
|
+
* A detector that got that wrong would not merely report a wrong number.
|
|
401
|
+
* `prune` *removes* examples, so a rule misread as an example becomes a
|
|
402
|
+
* proposal to delete an instruction — and it would arrive with the same
|
|
403
|
+
* confidence as a correct one.
|
|
404
|
+
*
|
|
405
|
+
* The tempting narrowing is to require the left side to be quoted. It was
|
|
406
|
+
* considered and refused: both probes are quoted **because the author of the
|
|
407
|
+
* probes wrote them that way**, and a threshold fitted to one's own fixture is
|
|
408
|
+
* a threshold that has been measured against nothing. What would settle it is
|
|
409
|
+
* real prompts using the shape, not a rule invented here.
|
|
410
|
+
*
|
|
411
|
+
* Until then the honest behaviour is the one `prune` now has: say what was
|
|
412
|
+
* looked for, so a prompt this cannot read reports that it was not seen rather
|
|
413
|
+
* than that it does not exist. That sentence used to read "this prompt has
|
|
414
|
+
* fewer than two few-shot examples", which was a confident false statement
|
|
415
|
+
* about a prompt with four of them.
|
|
416
|
+
*/
|
|
248
417
|
/** Splits the prompt into labelled example blocks. */
|
|
249
418
|
export function findExamples(prompt, count) {
|
|
250
419
|
const lines = prompt.split('\n');
|
|
420
|
+
const tagged = xmlExamples(prompt);
|
|
421
|
+
if (tagged.length >= 2)
|
|
422
|
+
return tagged.map((text) => ({ text, tokens: count(text) }));
|
|
251
423
|
for (const header of EXAMPLE_HEADER_TIERS) {
|
|
252
424
|
const blocks = splitOnHeader(lines, header);
|
|
253
425
|
if (blocks.length >= 2) {
|
|
@@ -306,6 +478,102 @@ function fencedBlocks(prompt) {
|
|
|
306
478
|
* prompts worth checking. Only keys at nesting depth 1 count, so a nested
|
|
307
479
|
* field name cannot be mistaken for a top-level one.
|
|
308
480
|
*/
|
|
481
|
+
/**
|
|
482
|
+
* Fence languages this reads a schema out of.
|
|
483
|
+
*
|
|
484
|
+
* `ts` and `typescript` are new; the rest were already here and three of them
|
|
485
|
+
* did not work. See `unquotedKeys`.
|
|
486
|
+
*/
|
|
487
|
+
const SCHEMA_LANGS = /^(?:json|jsonc|json5|yaml|yml|ts|typescript)$/;
|
|
488
|
+
/**
|
|
489
|
+
* Keys written without quotes, which is most of them.
|
|
490
|
+
*
|
|
491
|
+
* **The filter above already admitted `yaml`, `yml` and `json5`, and the
|
|
492
|
+
* extractor could not read any of them.** It only accepted a quoted string
|
|
493
|
+
* followed by a colon, which is JSON and nothing else — so a prompt showing its
|
|
494
|
+
* schema as YAML matched a language this module claims to support, produced
|
|
495
|
+
* zero keys, and reported no restatement however thoroughly the prose walked
|
|
496
|
+
* the fields. Not a missing feature: a promise made in one expression and
|
|
497
|
+
* broken by the next.
|
|
498
|
+
*
|
|
499
|
+
* `ts` is added at the same time because it is the same omission wearing a
|
|
500
|
+
* different label — `interface Ticket { category: string }` is how a
|
|
501
|
+
* TypeScript codebase writes the structured output it wants back, and it was
|
|
502
|
+
* not on the list at all.
|
|
503
|
+
*
|
|
504
|
+
* Two shapes, because YAML has no braces:
|
|
505
|
+
*
|
|
506
|
+
* - **Braced** (`json5`, `jsonc`, `ts`): an identifier at depth 1 followed by
|
|
507
|
+
* `:` or `?:`. Depth is what keeps a nested object's fields out.
|
|
508
|
+
* - **Indented** (`yaml`, `yml`): an identifier at the block's own outermost
|
|
509
|
+
* indent, followed by `:`. Anything indented past it belongs to a parent key
|
|
510
|
+
* and is not top-level.
|
|
511
|
+
*
|
|
512
|
+
* Deliberately not applied to a fence with **no** language. An unlabelled block
|
|
513
|
+
* is as likely to be a shell session or a log line as a schema, and
|
|
514
|
+
* `Note: something` reads as a key to any rule this simple. The quoted form
|
|
515
|
+
* stays the only thing read there, exactly as before.
|
|
516
|
+
*/
|
|
517
|
+
function unquotedKeys(body, lang) {
|
|
518
|
+
if (!SCHEMA_LANGS.test(lang))
|
|
519
|
+
return [];
|
|
520
|
+
if (/^(?:yaml|yml)$/.test(lang)) {
|
|
521
|
+
const lines = body
|
|
522
|
+
.split('\n')
|
|
523
|
+
.filter((line) => line.trim() !== '' && !/^\s*#/.test(line));
|
|
524
|
+
const outermost = Math.min(...lines.map((line) => line.length - line.trimStart().length));
|
|
525
|
+
if (!Number.isFinite(outermost))
|
|
526
|
+
return [];
|
|
527
|
+
return [
|
|
528
|
+
...new Set(lines
|
|
529
|
+
.filter((line) => line.length - line.trimStart().length === outermost)
|
|
530
|
+
.map((line) => /^\s*([A-Za-z_][\w-]*)\s*:/.exec(line))
|
|
531
|
+
.filter((found) => found !== null)
|
|
532
|
+
.map((found) => found[1])),
|
|
533
|
+
];
|
|
534
|
+
}
|
|
535
|
+
/*
|
|
536
|
+
Character by character rather than line by line, and that was found by
|
|
537
|
+
running it: `interface Ticket { category: string; urgency: number }` is one
|
|
538
|
+
line, so a line-oriented walk sees depth 0 where it starts and reads nothing
|
|
539
|
+
at all. Single-line objects are the common case in a `ts` fence and in the
|
|
540
|
+
JSON5 people paste, not the exception.
|
|
541
|
+
*/
|
|
542
|
+
const keys = [];
|
|
543
|
+
let depth = 0;
|
|
544
|
+
let candidate = '';
|
|
545
|
+
for (let i = 0; i < body.length; i++) {
|
|
546
|
+
const ch = body[i];
|
|
547
|
+
if (ch === '{' || ch === '[') {
|
|
548
|
+
depth++;
|
|
549
|
+
candidate = '';
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
if (ch === '}' || ch === ']') {
|
|
553
|
+
depth--;
|
|
554
|
+
candidate = '';
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
if (ch === ',' || ch === ';' || ch === '\n') {
|
|
558
|
+
candidate = '';
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
if (/[A-Za-z0-9_-]/.test(ch)) {
|
|
562
|
+
candidate += ch;
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
if (ch === ':' || (ch === '?' && body[i + 1] === ':')) {
|
|
566
|
+
if (depth === 1 && /^[A-Za-z_][\w-]*$/.test(candidate))
|
|
567
|
+
keys.push(candidate);
|
|
568
|
+
candidate = '';
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
// Whitespace between a name and its colon is fine; anything else is not a key.
|
|
572
|
+
if (!/\s/.test(ch))
|
|
573
|
+
candidate = '';
|
|
574
|
+
}
|
|
575
|
+
return [...new Set(keys)];
|
|
576
|
+
}
|
|
309
577
|
function topLevelKeys(body) {
|
|
310
578
|
const keys = [];
|
|
311
579
|
let depth = 0;
|
|
@@ -360,8 +628,8 @@ export function findRestatedFormat(prompt, count) {
|
|
|
360
628
|
const blocks = fencedBlocks(prompt);
|
|
361
629
|
const keys = [
|
|
362
630
|
...new Set(blocks
|
|
363
|
-
.filter((b) => b.lang === '' ||
|
|
364
|
-
.flatMap((b) => topLevelKeys(b.body))),
|
|
631
|
+
.filter((b) => b.lang === '' || SCHEMA_LANGS.test(b.lang))
|
|
632
|
+
.flatMap((b) => [...topLevelKeys(b.body), ...unquotedKeys(b.body, b.lang)])),
|
|
365
633
|
].filter((k) => k.length >= 3);
|
|
366
634
|
if (keys.length < RESTATED_KEY_MINIMUM)
|
|
367
635
|
return null;
|