@m6d/cortex-cli 0.23.0 → 0.24.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.
@@ -1,6 +1,8 @@
1
1
  import { z } from "zod";
2
2
  import { datasetIdentifierSchema } from "../datasets/index";
3
3
  import { canCarrySecret } from "../egress-policy/index";
4
+ import { isRecord } from "../json/index";
5
+ import { linearPattern } from "../pattern/index";
4
6
  import { triggerScheduleSchema } from "../schedule/index";
5
7
  import {
6
8
  displayNameSchema,
@@ -677,47 +679,251 @@ const propertyOrderSchema = z
677
679
  .refine((names) => new Set(names).size === names.length, "a name repeats")
678
680
  .optional();
679
681
 
680
- /** What an array holds: a scalar, or an object with properties of its own. */
681
- const itemsSchema = z.strictObject({
682
- type: z.enum([...SCALAR_TYPES, "object"]),
683
- get properties() {
684
- return z.record(propertyNameSchema, schemaPropertySchema).optional();
685
- },
686
- required: z.array(z.string()).optional(),
687
- "x-order": propertyOrderSchema,
682
+ const countSchema = z.number().int().min(0).optional();
683
+
684
+ /**
685
+ * The JSON Schema keywords that bound one value beyond its type and enum:
686
+ * a string's `pattern` (one RE2 can match, as Cortex checks it) and lengths,
687
+ * a scalar's `format` (kept for the model, not checked), a number's range and step, an
688
+ * array's length and uniqueness, and a scalar's `const`.
689
+ */
690
+ export const valueKeywordsSchema = z.strictObject({
691
+ pattern: z
692
+ .string()
693
+ .min(1)
694
+ .max(1_000)
695
+ .refine((pattern) => linearPattern(pattern) !== undefined, "a pattern Cortex cannot match")
696
+ .optional(),
697
+ minLength: countSchema,
698
+ maxLength: countSchema,
699
+ format: z.string().trim().min(1).max(100).optional(),
700
+ minimum: z.number().optional(),
701
+ maximum: z.number().optional(),
702
+ exclusiveMinimum: z.number().optional(),
703
+ exclusiveMaximum: z.number().optional(),
704
+ multipleOf: z.number().positive().optional(),
705
+ minItems: countSchema,
706
+ maxItems: countSchema,
707
+ uniqueItems: z.boolean().optional(),
708
+ const: z.union([z.string().max(4_000), z.number(), z.boolean()]).optional(),
688
709
  });
710
+ export type ValueKeywords = z.infer<typeof valueKeywordsSchema>;
711
+
712
+ const NUMERIC_TYPES = ["number", "integer"];
713
+
714
+ /** The types each value keyword bounds; on any other it means nothing. */
715
+ export const VALUE_KEYWORD_TYPES = {
716
+ pattern: ["string"],
717
+ minLength: ["string"],
718
+ maxLength: ["string"],
719
+ format: SCALAR_TYPES,
720
+ minimum: NUMERIC_TYPES,
721
+ maximum: NUMERIC_TYPES,
722
+ exclusiveMinimum: NUMERIC_TYPES,
723
+ exclusiveMaximum: NUMERIC_TYPES,
724
+ multipleOf: NUMERIC_TYPES,
725
+ minItems: ["array"],
726
+ maxItems: ["array"],
727
+ uniqueItems: ["array"],
728
+ const: SCALAR_TYPES,
729
+ } satisfies Record<keyof ValueKeywords, ReadonlyArray<string>>;
730
+
731
+ /** The types a rule tests a value for: `"null"` too, since a rule may tell a null apart. */
732
+ const ruleTypeSchema = z.enum([...SCALAR_TYPES, "object", "array", "null"]);
733
+ type RuleType = z.infer<typeof ruleTypeSchema>;
734
+
735
+ /**
736
+ * An object's cross-field rules: which properties go together (`anyOf`,
737
+ * `oneOf`, `allOf`, `not`, `if`/`then`/`else`, `dependentRequired`,
738
+ * `dependentSchemas`) and how many it holds. A rule names the object's own
739
+ * properties only.
740
+ */
741
+ export type ObjectRules = {
742
+ anyOf?: Condition[];
743
+ oneOf?: Condition[];
744
+ allOf?: Condition[];
745
+ not?: Condition;
746
+ if?: Condition;
747
+ then?: Condition;
748
+ else?: Condition;
749
+ dependentRequired?: Record<string, string[]>;
750
+ dependentSchemas?: Record<string, Condition>;
751
+ minProperties?: number;
752
+ maxProperties?: number;
753
+ };
754
+
755
+ /**
756
+ * A schema a rule checks its object against, in the part of JSON Schema a
757
+ * rule reads: what it requires, its properties' types, enums and value
758
+ * keywords, an array's items, and rules of its own.
759
+ */
760
+ type Condition = ValueKeywords &
761
+ ObjectRules & {
762
+ type?: RuleType | RuleType[];
763
+ enum?: Array<string | number | boolean | null>;
764
+ required?: string[];
765
+ properties?: Record<string, Condition>;
766
+ items?: Condition;
767
+ };
768
+
769
+ const conditionSchema: z.ZodType<Condition, Condition> = z
770
+ .lazy(() =>
771
+ z.strictObject({
772
+ type: z.union([ruleTypeSchema, z.array(ruleTypeSchema).min(1)]).optional(),
773
+ enum: z
774
+ .array(z.union([z.string(), z.number(), z.boolean(), z.null()]))
775
+ .min(1)
776
+ .optional(),
777
+ ...valueKeywordsSchema.shape,
778
+ required: z.array(z.string()).optional(),
779
+ properties: z.record(z.string(), conditionSchema).optional(),
780
+ items: conditionSchema.optional(),
781
+ ...objectRulesSchema.shape,
782
+ }),
783
+ )
784
+ .meta({ id: "SchemaCondition" });
785
+
786
+ /** The members of a rule's `anyOf`, `oneOf` or `allOf`. */
787
+ const conditionsSchema = z.array(conditionSchema).min(1).meta({ id: "SchemaConditions" });
788
+
789
+ export const objectRulesSchema = z.strictObject({
790
+ anyOf: conditionsSchema.optional(),
791
+ oneOf: conditionsSchema.optional(),
792
+ allOf: conditionsSchema.optional(),
793
+ not: conditionSchema.optional(),
794
+ if: conditionSchema.optional(),
795
+ then: conditionSchema.optional(),
796
+ else: conditionSchema.optional(),
797
+ dependentRequired: z.record(z.string(), z.array(z.string())).optional(),
798
+ dependentSchemas: z.record(z.string(), conditionSchema).optional(),
799
+ minProperties: countSchema,
800
+ maxProperties: countSchema,
801
+ }) satisfies z.ZodType<ObjectRules>;
802
+
803
+ /** The keywords that check an object's properties against each other. */
804
+ export const OBJECT_RULE_KEYWORDS = objectRulesSchema.keyof().options;
805
+
806
+ /** A rule keyword's array value, or none when it holds no array. */
807
+ function listed(value: unknown) {
808
+ return Array.isArray(value) ? (value as unknown[]) : [];
809
+ }
810
+
811
+ /** A rule keyword's map value, or an empty one when it holds no map. */
812
+ function keyed(value: unknown) {
813
+ return isRecord(value) ? value : {};
814
+ }
815
+
816
+ /**
817
+ * A rule and every rule nested in it that checks the same object, through
818
+ * its combinators, `not`, `if`/`then`/`else` and `dependentSchemas`, but
819
+ * not inside a property's own value.
820
+ */
821
+ export function ruleNodes(rule: unknown): Array<Record<string, unknown>> {
822
+ if (!isRecord(rule)) return [];
823
+ return [
824
+ rule,
825
+ ...[
826
+ ...["anyOf", "oneOf", "allOf"].flatMap((keyword) => listed(rule[keyword])),
827
+ rule.not,
828
+ rule.if,
829
+ rule.then,
830
+ rule.else,
831
+ ...Object.values(keyed(rule.dependentSchemas)),
832
+ ].flatMap(ruleNodes),
833
+ ];
834
+ }
835
+
836
+ /**
837
+ * The property names a rule reaches on the object it checks: those any of
838
+ * its nodes (`ruleNodes`) requires, reads or makes depend on each other.
839
+ */
840
+ export function ruleNames(rule: unknown) {
841
+ return ruleNodes(rule)
842
+ .flatMap((node) => [
843
+ ...listed(node.required),
844
+ ...Object.keys(keyed(node.properties)),
845
+ ...Object.entries(keyed(node.dependentRequired)).flatMap(([name, names]) => [
846
+ name,
847
+ ...listed(names),
848
+ ]),
849
+ ...Object.keys(keyed(node.dependentSchemas)),
850
+ ])
851
+ .filter((name): name is string => typeof name === "string");
852
+ }
853
+
854
+ /** What an array holds: a scalar (a string's values listed in `enum`, as a property's), or an object with properties of its own; with `"null"` beside its type when an item may be null. */
855
+ type SchemaItems = ValueKeywords &
856
+ ObjectRules & {
857
+ type: ItemType | [ItemType, "null"];
858
+ enum?: Array<string | null>;
859
+ properties?: Record<string, SchemaProperty>;
860
+ required?: string[];
861
+ "x-order"?: string[];
862
+ };
689
863
 
690
864
  /**
691
865
  * A JSON Schema property, in the part of JSON Schema a field editor holds:
692
866
  * a type (with `"null"` beside it when the value may be null), an `enum` of
693
867
  * strings for a string, an object's `properties`, `required` and the
694
- * `x-order` they are shown in, an array's `items`, a `description`, and
695
- * the extensions `x-sensitive` (redacted from logs), `x-source` (top-level
696
- * scalar fields of a tool's input only) and `x-target` (top-level fields of
697
- * a tool's input only).
868
+ * `x-order` they are shown in, an array's `items`, a `description`, the
869
+ * value keywords its type takes (`VALUE_KEYWORD_TYPES`), an object's rules,
870
+ * and the extensions `x-sensitive` (redacted from logs), `x-source`
871
+ * (top-level scalar fields of a tool's input only) and `x-target` (top-level
872
+ * fields of a tool's input only).
698
873
  */
699
- const schemaPropertySchema = z.strictObject({
700
- type: z.union([schemaTypeSchema, z.tuple([schemaTypeSchema, z.literal("null")])]),
701
- description: z.string().trim().max(FIELD_LIMITS.descriptionLength).optional(),
702
- /** A nullable enum lists `null` beside its values, as the console stores one. */
703
- enum: z
704
- .array(z.union([z.string().trim().min(1), z.null()]))
705
- .min(1)
706
- .max(FIELD_LIMITS.enumValues + 1)
707
- .optional(),
708
- get properties() {
709
- return z.record(propertyNameSchema, schemaPropertySchema).optional();
710
- },
711
- required: z.array(z.string()).optional(),
712
- "x-order": propertyOrderSchema,
713
- get items() {
714
- return itemsSchema.optional();
715
- },
716
- "x-sensitive": z.boolean().optional(),
717
- "x-source": fieldSourceSchema.optional(),
718
- "x-target": fieldTargetSchema.optional(),
719
- });
720
- type SchemaProperty = z.infer<typeof schemaPropertySchema>;
874
+ type SchemaProperty = ValueKeywords &
875
+ ObjectRules & {
876
+ type: SchemaType | [SchemaType, "null"];
877
+ description?: string;
878
+ /** A nullable enum lists `null` beside its values, as the console stores one. */
879
+ enum?: Array<string | null>;
880
+ properties?: Record<string, SchemaProperty>;
881
+ required?: string[];
882
+ "x-order"?: string[];
883
+ items?: SchemaItems;
884
+ "x-sensitive"?: boolean;
885
+ "x-source"?: FieldSource;
886
+ "x-target"?: FieldTarget;
887
+ };
888
+ type SchemaType = z.infer<typeof schemaTypeSchema>;
889
+ const itemTypeSchema = z.enum([...SCALAR_TYPES, "object"]);
890
+ type ItemType = z.infer<typeof itemTypeSchema>;
891
+
892
+ /** A nullable enum lists `null` beside its values, as the console stores one. */
893
+ const enumSchema = z
894
+ .array(z.union([z.string().trim().min(1), z.null()]))
895
+ .min(1)
896
+ .max(FIELD_LIMITS.enumValues + 1)
897
+ .optional();
898
+
899
+ const itemsSchema: z.ZodType<SchemaItems, SchemaItems> = z.lazy(() =>
900
+ z.strictObject({
901
+ type: z.union([itemTypeSchema, z.tuple([itemTypeSchema, z.literal("null")])]),
902
+ enum: enumSchema,
903
+ properties: z.record(propertyNameSchema, schemaPropertySchema).optional(),
904
+ required: z.array(z.string()).optional(),
905
+ "x-order": propertyOrderSchema,
906
+ ...valueKeywordsSchema.shape,
907
+ ...objectRulesSchema.shape,
908
+ }),
909
+ );
910
+
911
+ const schemaPropertySchema: z.ZodType<SchemaProperty, SchemaProperty> = z.lazy(() =>
912
+ z.strictObject({
913
+ type: z.union([schemaTypeSchema, z.tuple([schemaTypeSchema, z.literal("null")])]),
914
+ description: z.string().trim().max(FIELD_LIMITS.descriptionLength).optional(),
915
+ enum: enumSchema,
916
+ properties: z.record(propertyNameSchema, schemaPropertySchema).optional(),
917
+ required: z.array(z.string()).optional(),
918
+ "x-order": propertyOrderSchema,
919
+ items: itemsSchema.optional(),
920
+ "x-sensitive": z.boolean().optional(),
921
+ "x-source": fieldSourceSchema.optional(),
922
+ "x-target": fieldTargetSchema.optional(),
923
+ ...valueKeywordsSchema.shape,
924
+ ...objectRulesSchema.shape,
925
+ }),
926
+ );
721
927
 
722
928
  /** A node's own type, without the `"null"` beside it. */
723
929
  function typeOf(node: { type: SchemaProperty["type"] }) {
@@ -728,29 +934,45 @@ function typeOf(node: { type: SchemaProperty["type"] }) {
728
934
  type RowIssue = { at: Array<string | number>; message: string };
729
935
 
730
936
  /** A node holding properties: a property, an array's items, or an input or output schema. */
731
- type ObjectNode = {
732
- type: SchemaProperty["type"];
733
- properties?: Record<string, SchemaProperty>;
734
- required?: string[];
735
- "x-order"?: string[];
736
- };
937
+ type ObjectNode = ObjectRules &
938
+ ValueKeywords & {
939
+ type: SchemaProperty["type"];
940
+ properties?: Record<string, SchemaProperty>;
941
+ required?: string[];
942
+ "x-order"?: string[];
943
+ };
737
944
 
738
- /** What a node's own keywords break: `properties`, `required` or `x-order` off an object, or the last two naming no property. */
945
+ /**
946
+ * What a node's own keywords break: `properties`, `required`, `x-order` or
947
+ * a rule off an object, the next two or a rule naming no property, and a
948
+ * value keyword off the types it bounds.
949
+ */
739
950
  function nodeIssues(node: ObjectNode) {
740
951
  const issues = new Array<RowIssue>();
741
- const object = typeOf(node) === "object";
742
- for (const keyword of ["properties", "required", "x-order"] as const) {
952
+ const type = typeOf(node);
953
+ const object = type === "object";
954
+ for (const keyword of ["properties", "required", "x-order", ...OBJECT_RULE_KEYWORDS] as const) {
743
955
  if (!object && node[keyword] !== undefined) {
744
956
  issues.push({ at: [keyword], message: "only an object has it" });
745
957
  }
746
958
  }
959
+ const named = (name: string) => Object.hasOwn(node.properties ?? {}, name);
747
960
  for (const keyword of ["required", "x-order"] as const) {
748
961
  node[keyword]?.forEach((name, index) => {
749
- if (!Object.hasOwn(node.properties ?? {}, name)) {
750
- issues.push({ at: [keyword, index], message: "names no property" });
751
- }
962
+ if (!named(name)) issues.push({ at: [keyword, index], message: "names no property" });
752
963
  });
753
964
  }
965
+ for (const keyword of OBJECT_RULE_KEYWORDS) {
966
+ if (object && !ruleNames({ [keyword]: node[keyword] }).every(named)) {
967
+ issues.push({ at: [keyword], message: "names no property" });
968
+ }
969
+ }
970
+ for (const [keyword, types] of Object.entries(VALUE_KEYWORD_TYPES)) {
971
+ if (node[keyword as keyof ValueKeywords] === undefined) continue;
972
+ if (!types.some((bounded) => bounded === type)) {
973
+ issues.push({ at: [keyword], message: `only a ${types.join(" or ")} has it` });
974
+ }
975
+ }
754
976
  return issues;
755
977
  }
756
978
 
@@ -759,7 +981,7 @@ function nodeIssues(node: ObjectNode) {
759
981
  * them, beside `null` exactly when the type allows null, as the console
760
982
  * stores a nullable one.
761
983
  */
762
- function enumIssueOf(property: SchemaProperty, type: string | undefined) {
984
+ function enumIssueOf(property: Pick<SchemaProperty, "enum" | "type">, type: string | undefined) {
763
985
  if (property.enum === undefined) return undefined;
764
986
  if (type !== "string") return "an enum lists a string's values";
765
987
  const values = property.enum.filter((value) => value !== null);
@@ -854,7 +1076,10 @@ function heldByRows(
854
1076
  const at = [...path, "properties", name];
855
1077
  report(at, propertyIssues(property, bindings));
856
1078
  heldByRows(property, context, at, undefined);
857
- if (property.items) heldByRows(property.items, context, [...at, "items"], undefined);
1079
+ if (!property.items) continue;
1080
+ const enumIssue = enumIssueOf(property.items, typeOf(property.items));
1081
+ if (enumIssue) report([...at, "items"], [{ at: ["enum"], message: enumIssue }]);
1082
+ heldByRows(property.items, context, [...at, "items"], undefined);
858
1083
  }
859
1084
  }
860
1085
 
@@ -862,7 +1087,8 @@ function heldByRows(
862
1087
  * An object's JSON Schema, the shape of a tool's or trigger's input and
863
1088
  * output, and of what the console's field rows write. `bindings`, given for
864
1089
  * a tool's input, are the extensions its top-level fields may carry:
865
- * `x-source` on a scalar, `x-target` on any field.
1090
+ * `x-source` on a scalar, `x-target` on any field. Only a tool's input holds
1091
+ * rules at its root, as the console keeps them beside its rows.
866
1092
  */
867
1093
  export function objectJsonSchemaOf(bindings?: ReadonlyArray<Binding>) {
868
1094
  return z
@@ -871,8 +1097,20 @@ export function objectJsonSchemaOf(bindings?: ReadonlyArray<Binding>) {
871
1097
  properties: z.record(propertyNameSchema, schemaPropertySchema).optional(),
872
1098
  required: z.array(z.string()).optional(),
873
1099
  "x-order": propertyOrderSchema,
1100
+ ...objectRulesSchema.shape,
874
1101
  })
875
- .superRefine((schema, context) => heldByRows(schema, context, [], bindings));
1102
+ .superRefine((schema, context) => {
1103
+ heldByRows(schema, context, [], bindings);
1104
+ if (bindings !== undefined) return;
1105
+ for (const keyword of OBJECT_RULE_KEYWORDS) {
1106
+ if (schema[keyword] === undefined) continue;
1107
+ context.addIssue({
1108
+ code: "custom",
1109
+ path: [keyword],
1110
+ message: "only a tool's input has rules at its root",
1111
+ });
1112
+ }
1113
+ });
876
1114
  }
877
1115
  const objectSchema = objectJsonSchemaOf();
878
1116
 
@@ -0,0 +1,276 @@
1
+ // cspell:ignore fefe fff
2
+ import { RE2JS } from "re2js";
3
+
4
+ /*
5
+ * A tool input schema's `pattern`: a regular expression JavaScript reads with
6
+ * no flags, which Cortex matches on RE2 so a value is checked in time linear
7
+ * in its length. A pattern RE2 cannot match as JavaScript reads it is refused
8
+ * by Cortex and left out of what the TypeScript SDK exports.
9
+ *
10
+ * RE2 matches code points where JavaScript matches UTF-16 units, so the
11
+ * pattern and every input lift each surrogate unit into a private-use plane,
12
+ * where it is one code point: RE2 then reads the units JavaScript reads.
13
+ * Nothing collides there, since a real character in that plane is astral,
14
+ * arrives as two surrogate units and is lifted in turn.
15
+ */
16
+
17
+ /**
18
+ * What one escape or character in a class stands for, spelled as the inside
19
+ * of an RE2 class: a single character carries its UTF-16 unit, and an
20
+ * unescaped `-` is marked as one that may make a range.
21
+ */
22
+ type Atom = { body: string; char?: number; dash?: boolean };
23
+
24
+ /** JavaScript's `\s`, its WhiteSpace and LineTerminator characters, as the inside of an RE2 class. */
25
+ const SPACE =
26
+ "\\x{9}-\\x{d}\\x{20}\\x{a0}\\x{1680}\\x{2000}-\\x{200a}\\x{2028}\\x{2029}\\x{202f}\\x{205f}\\x{3000}\\x{feff}";
27
+ /** Every character `SPACE` leaves out: JavaScript's `\S`. */
28
+ const NON_SPACE =
29
+ "\\x{0}-\\x{8}\\x{e}-\\x{1f}\\x{21}-\\x{9f}\\x{a1}-\\x{167f}\\x{1681}-\\x{1fff}\\x{200b}-\\x{2027}" +
30
+ "\\x{202a}-\\x{202e}\\x{2030}-\\x{205e}\\x{2060}-\\x{2fff}\\x{3001}-\\x{fefe}\\x{ff00}-\\x{10ffff}";
31
+ /** The set escapes as JavaScript reads them; RE2's own `\d` and `\w` match, its `\s` holds only ASCII. */
32
+ const SET_ESCAPES = new Map([
33
+ ["d", "\\d"],
34
+ ["D", "\\D"],
35
+ ["w", "\\w"],
36
+ ["W", "\\W"],
37
+ ["s", SPACE],
38
+ ["S", NON_SPACE],
39
+ ]);
40
+ /** JavaScript's `.`: every character but a line terminator, where RE2's leaves out only `\n`. */
41
+ const DOT = "[^\\x{a}\\x{d}\\x{2028}\\x{2029}]";
42
+ const CONTROL_ESCAPES = new Map([
43
+ ["f", 0x0c],
44
+ ["n", 0x0a],
45
+ ["r", 0x0d],
46
+ ["t", 0x09],
47
+ ["v", 0x0b],
48
+ ]);
49
+ const QUANTIFIER = /\{\d+(?:,\d*)?\}/y;
50
+ const SURROGATE = /[\uD800-\uDFFF]/g;
51
+ /** Where surrogate unit `0xd800 + n` is lifted: code point `0xf0000 + n`. */
52
+ const LIFTED_SURROGATES = 0xf0000;
53
+ const HEX = /[0-9A-Fa-f]+/y;
54
+ /** What JavaScript's `[^]` and `[]` hold: every character, and none. */
55
+ const ANY = "[\\x{0}-\\x{10ffff}]";
56
+ const NONE = "[^\\x{0}-\\x{10ffff}]";
57
+
58
+ /** A thrown error: the pattern holds what RE2 cannot match as JavaScript reads it. */
59
+ function unsupported(what: string): never {
60
+ throw new Error(`a pattern with ${what}`);
61
+ }
62
+
63
+ /** A UTF-16 unit as the one code point RE2 reads: a surrogate lifted, any other unit itself. */
64
+ function lifted(unit: number) {
65
+ return unit >= 0xd800 && unit <= 0xdfff ? LIFTED_SURROGATES + unit - 0xd800 : unit;
66
+ }
67
+
68
+ /** `input` with every surrogate unit lifted, one code point per UTF-16 unit. */
69
+ function liftedText(input: string) {
70
+ return input.replace(SURROGATE, (unit) => String.fromCodePoint(lifted(unit.charCodeAt(0))));
71
+ }
72
+
73
+ /** One UTF-16 unit as RE2 reads it literally, inside a class or out. */
74
+ function literal(unit: number) {
75
+ const char = lifted(unit);
76
+ const spelled = String.fromCodePoint(char);
77
+ return /^[A-Za-z0-9]$/.test(spelled) ? spelled : `\\x{${char.toString(16)}}`;
78
+ }
79
+
80
+ /**
81
+ * The units from `from` to `to` as the inside of an RE2 class: the part
82
+ * among the surrogates, lifted, apart from the parts below and above it.
83
+ */
84
+ function unitRange(from: number, to: number) {
85
+ return (
86
+ [
87
+ [from, Math.min(to, 0xd7ff)],
88
+ [Math.max(from, 0xd800), Math.min(to, 0xdfff)],
89
+ [Math.max(from, 0xe000), to],
90
+ ] satisfies Array<[number, number]>
91
+ )
92
+ .filter(([low, high]) => low <= high)
93
+ .map(([low, high]) => `${literal(low)}-${literal(high)}`)
94
+ .join("");
95
+ }
96
+
97
+ function character(char: number, dash = false) {
98
+ return { body: literal(char), char, dash };
99
+ }
100
+
101
+ /**
102
+ * A class's atoms as the inside of an RE2 class: an unescaped dash between
103
+ * two characters makes a range, and beside a set escape it is a character,
104
+ * as JavaScript reads them.
105
+ */
106
+ function classBody(atoms: Atom[]) {
107
+ let body = "";
108
+ for (let index = 0; index < atoms.length; index += 1) {
109
+ const [from, dash, to] = atoms.slice(index, index + 3);
110
+ if (!from) break;
111
+ if (!dash?.dash || !to) {
112
+ body += from.body;
113
+ continue;
114
+ }
115
+ body +=
116
+ from.char !== undefined && to.char !== undefined
117
+ ? unitRange(from.char, to.char)
118
+ : `${from.body}${dash.body}${to.body}`;
119
+ index += 2;
120
+ }
121
+ return body;
122
+ }
123
+
124
+ /**
125
+ * A pattern read left to right as JavaScript reads it with no flags (Annex B
126
+ * included), written in RE2's syntax; throws on a lookaround, a
127
+ * backreference, a legacy octal escape, a group modifier or a `\c` without
128
+ * its letter. It reads UTF-16 units, as JavaScript does. Groups do not
129
+ * capture: nothing reads them.
130
+ */
131
+ class Translation {
132
+ private at = 0;
133
+
134
+ public constructor(private readonly pattern: string) {}
135
+
136
+ public spell() {
137
+ let re2 = "";
138
+ while (this.at < this.pattern.length) re2 += this.term();
139
+ return re2;
140
+ }
141
+
142
+ private term() {
143
+ const char = this.pattern[this.at] ?? "";
144
+ if (char === "\\") return this.escapeOutsideClass();
145
+ if (char === "[") return this.characterClass();
146
+ if (char === "(") return this.group();
147
+ if (char === "{") return this.brace();
148
+ if (!"|)^$*+?.".includes(char)) return literal(this.unit());
149
+ this.at += 1;
150
+ return char === "." ? DOT : char;
151
+ }
152
+
153
+ private escapeOutsideClass() {
154
+ this.at += 1;
155
+ const assertion = this.pattern[this.at];
156
+ if (assertion === "b" || assertion === "B") {
157
+ this.at += 1;
158
+ return `\\${assertion}`;
159
+ }
160
+ const atom = this.escape(false);
161
+ return "char" in atom ? atom.body : `[${atom.body}]`;
162
+ }
163
+
164
+ /** The escape after a backslash, which `at` is past; in a class `\b` is a backspace. */
165
+ private escape(insideClass: boolean) {
166
+ const next = this.pattern[this.at] ?? unsupported("a trailing backslash");
167
+ this.at += 1;
168
+ const set = SET_ESCAPES.get(next);
169
+ if (set) return { body: set };
170
+ const control = insideClass && next === "b" ? 0x08 : CONTROL_ESCAPES.get(next);
171
+ if (control !== undefined) return character(control);
172
+ if (next === "c") return character(this.controlLetter(insideClass));
173
+ if (next === "x" || next === "u") {
174
+ return character(this.hex(next === "x" ? 2 : 4) ?? next.charCodeAt(0));
175
+ }
176
+ if (next === "0" && !/^\d$/.test(this.pattern[this.at] ?? "")) return character(0);
177
+ if (/^[\dk]$/.test(next)) unsupported("a backreference or an octal escape");
178
+ this.at -= 1;
179
+ return character(this.unit());
180
+ }
181
+
182
+ /** The control character a `\c` names: a letter, or in a class also a digit or `_`. */
183
+ private controlLetter(insideClass: boolean) {
184
+ const letter = this.pattern[this.at] ?? "";
185
+ if (!(insideClass ? /^\w$/ : /^[A-Za-z]$/).test(letter)) {
186
+ unsupported("a \\c without its letter");
187
+ }
188
+ this.at += 1;
189
+ return letter.charCodeAt(0) % 32;
190
+ }
191
+
192
+ /** `length` hex digits at `at` as a UTF-16 unit, consumed; undefined when fewer follow. */
193
+ private hex(length: number) {
194
+ HEX.lastIndex = this.at;
195
+ const digits = HEX.exec(this.pattern)?.[0] ?? "";
196
+ if (digits.length < length) return undefined;
197
+ this.at += length;
198
+ return Number.parseInt(digits.slice(0, length), 16);
199
+ }
200
+
201
+ private unit() {
202
+ const unit = this.pattern.charCodeAt(this.at);
203
+ if (Number.isNaN(unit)) unsupported("a pattern cut short");
204
+ this.at += 1;
205
+ return unit;
206
+ }
207
+
208
+ /**
209
+ * A class, `at` on its `[`. In JavaScript a `]` always closes one, so
210
+ * `[]` holds nothing and `[^]` everything.
211
+ */
212
+ private characterClass() {
213
+ this.at += 1;
214
+ const negated = this.pattern[this.at] === "^";
215
+ if (negated) this.at += 1;
216
+ const atoms: Atom[] = [];
217
+ while (this.pattern[this.at] !== "]") {
218
+ if (this.at >= this.pattern.length) unsupported("an unclosed class");
219
+ atoms.push(this.classAtom());
220
+ }
221
+ this.at += 1;
222
+ const body = classBody(atoms);
223
+ if (!body) return negated ? ANY : NONE;
224
+ return `[${negated ? "^" : ""}${body}]`;
225
+ }
226
+
227
+ private classAtom() {
228
+ const char = this.pattern[this.at];
229
+ this.at += 1;
230
+ if (char === "\\") return this.escape(true);
231
+ if (char === "-") return character(0x2d, true);
232
+ this.at -= 1;
233
+ return character(this.unit());
234
+ }
235
+
236
+ /** A group's opening as RE2 reads it: every group, named or not, without a capture. */
237
+ private group() {
238
+ const opening = this.pattern.slice(this.at, this.at + 4);
239
+ if (/^\(\?<[^=!]/.test(opening)) {
240
+ const name = this.pattern.indexOf(">", this.at);
241
+ if (name < 0) unsupported("an unclosed group name");
242
+ this.at = name + 1;
243
+ } else if (opening.startsWith("(?")) {
244
+ if (!opening.startsWith("(?:")) unsupported("a lookaround or a group modifier");
245
+ this.at += 3;
246
+ } else {
247
+ this.at += 1;
248
+ }
249
+ return "(?:";
250
+ }
251
+
252
+ /** A quantifier, or a brace JavaScript reads as a character because it opens none. */
253
+ private brace() {
254
+ QUANTIFIER.lastIndex = this.at;
255
+ const quantifier = QUANTIFIER.exec(this.pattern)?.[0];
256
+ this.at += quantifier?.length ?? 1;
257
+ return quantifier ?? literal(0x7b);
258
+ }
259
+ }
260
+
261
+ /**
262
+ * A matcher for a pattern JavaScript compiles, meaning what the pattern
263
+ * means to JavaScript with no flags, UTF-16 unit for unit; undefined when
264
+ * RE2 cannot match it so, as for a lookaround, a backreference or a
265
+ * repetition past 1000. Its `test` runs on RE2, in time linear in the input.
266
+ * It never calls `new RegExp`, so it works while a caller has replaced that
267
+ * global.
268
+ */
269
+ export function linearPattern(pattern: string) {
270
+ try {
271
+ const re2 = RE2JS.compile(new Translation(pattern).spell());
272
+ return { test: (input: string) => re2.test(liftedText(input)) };
273
+ } catch {
274
+ return undefined;
275
+ }
276
+ }