@quonfig/cli 0.4.0 → 0.5.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 +157 -75
- package/dist/agent/skill-bundle.generated.d.ts +1 -1
- package/dist/agent/skill-bundle.generated.js +4 -4
- package/dist/codegen/code-generators/base-typescript-generator.d.ts +1 -1
- package/dist/codegen/schema-extractor.d.ts +1 -1
- package/dist/commands/config-schema.js +9 -2
- package/dist/commands/create.d.ts +3 -1
- package/dist/commands/create.js +93 -20
- package/dist/commands/get.d.ts +1 -0
- package/dist/commands/get.js +40 -3
- package/dist/commands/info.js +10 -1
- package/dist/commands/set-default.d.ts +2 -0
- package/dist/commands/set-default.js +70 -18
- package/dist/index.d.ts +6 -0
- package/dist/index.js +35 -3
- package/dist/interactive-prompt.d.ts +18 -0
- package/dist/interactive-prompt.js +11 -2
- package/dist/migrate/quonfig-target/operators.js +4 -2
- package/dist/migrate/quonfig-target/ruleset.d.ts +48 -0
- package/dist/migrate/quonfig-target/ruleset.js +132 -8
- package/dist/migrate/sources/flagsmith/translate.js +11 -0
- package/dist/migrate/sources/launchdarkly/translate.js +19 -16
- package/dist/push/git-pack.d.ts +12 -0
- package/dist/push/git-pack.js +27 -3
- package/dist/push/run-push.d.ts +2 -1
- package/dist/push/run-push.js +1 -2
- package/dist/ui/get-string.d.ts +18 -1
- package/dist/ui/get-string.js +20 -5
- package/dist/ui/get-value.d.ts +3 -1
- package/dist/ui/get-value.js +7 -7
- package/dist/util/clone-and-stack-push.js +8 -3
- package/dist/util/encryption.js +11 -20
- package/dist/util/git-ops.d.ts +4 -2
- package/dist/util/git-ops.js +4 -2
- package/dist/util/redact.d.ts +56 -0
- package/dist/util/redact.js +98 -0
- package/dist/util/value-source.d.ts +30 -0
- package/dist/util/value-source.js +81 -0
- package/dist/verify/regex-dialect.d.ts +39 -0
- package/dist/verify/regex-dialect.js +278 -0
- package/dist/verify/standalone.d.ts +3 -1
- package/dist/verify/standalone.js +4 -19
- package/dist/verify/validate.js +20 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/oclif.manifest.json +236 -192
- package/package.json +18 -16
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Quonfig regex v1": the regex dialect of PROP_MATCHES and PROP_DOES_NOT_MATCH
|
|
3
|
+
* (qfg-ktkr.2). Spec: project/plans/2026-10-12-regex-write-time-check.md
|
|
4
|
+
* section 2, approved 2026-10-16 (qfg-goi1.10.10). Fixture of record:
|
|
5
|
+
* integration-test-data tests/regex/dialect.yaml (tag v2026.10.22.1; \_, \v
|
|
6
|
+
* and \x80-\xFF rejected per qfg-ktkr.7), which
|
|
7
|
+
* test/verify/regex-dialect.test.ts holds this file to, row by row.
|
|
8
|
+
*
|
|
9
|
+
* The dialect is the intersection of what every SDK's engine (go RE2, node
|
|
10
|
+
* RegExp, python re, ruby Onigmo, java.util.regex, .NET Regex) accepts and
|
|
11
|
+
* reads the same way, plus a ReDoS safety rule. SDKs do not validate; the
|
|
12
|
+
* writers (this check, app-quonfig's copy, and the Git hook built from this
|
|
13
|
+
* repo) are the only gate.
|
|
14
|
+
*
|
|
15
|
+
* No imports: src/verify/ is bundled into the standalone qfg-verify hook.
|
|
16
|
+
*/
|
|
17
|
+
export declare const REGEX_V1_LIMITS: {
|
|
18
|
+
readonly maxLength: 256;
|
|
19
|
+
readonly maxRepeat: 1000;
|
|
20
|
+
readonly maxUnboundedQuantifiers: 2;
|
|
21
|
+
};
|
|
22
|
+
/** The canonical docs section the error links to (qfg-ktkr.5 owns the page). */
|
|
23
|
+
export declare const REGEX_V1_DOCS_URL = "https://docs.quonfig.com/docs/explanations/features/rules-and-segmentation#quonfig-regex-v1";
|
|
24
|
+
export type RegexV1Reason = 'backref' | 'bad-repeat' | 'class-syntax' | 'group-extension' | 'inline-flag' | 'lookaround' | 'named-group' | 'nested-quantifier' | 'octal' | 'possessive' | 'syntax' | 'too-long' | 'too-many-unbounded' | 'unicode-shorthand' | 'unsupported-escape';
|
|
25
|
+
/** Fix-it hints, verbatim from the fixture's `hints` map. */
|
|
26
|
+
export declare const REGEX_V1_HINTS: Record<RegexV1Reason, string>;
|
|
27
|
+
export interface RegexV1Violation {
|
|
28
|
+
hint: string;
|
|
29
|
+
reason: RegexV1Reason;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Check one pattern against Quonfig regex v1. Returns null when the pattern is
|
|
33
|
+
* in the dialect, or the reason and fix-it hint for the first violation.
|
|
34
|
+
*
|
|
35
|
+
* Precedence (fixture header): too-long first; then one left-to-right scan
|
|
36
|
+
* reports the first offending construct (an unclosed ( or [ is found at the
|
|
37
|
+
* end); then too-many-unbounded, which needs the whole pattern.
|
|
38
|
+
*/
|
|
39
|
+
export declare function checkRegexV1(pattern: string): null | RegexV1Violation;
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Quonfig regex v1": the regex dialect of PROP_MATCHES and PROP_DOES_NOT_MATCH
|
|
3
|
+
* (qfg-ktkr.2). Spec: project/plans/2026-10-12-regex-write-time-check.md
|
|
4
|
+
* section 2, approved 2026-10-16 (qfg-goi1.10.10). Fixture of record:
|
|
5
|
+
* integration-test-data tests/regex/dialect.yaml (tag v2026.10.22.1; \_, \v
|
|
6
|
+
* and \x80-\xFF rejected per qfg-ktkr.7), which
|
|
7
|
+
* test/verify/regex-dialect.test.ts holds this file to, row by row.
|
|
8
|
+
*
|
|
9
|
+
* The dialect is the intersection of what every SDK's engine (go RE2, node
|
|
10
|
+
* RegExp, python re, ruby Onigmo, java.util.regex, .NET Regex) accepts and
|
|
11
|
+
* reads the same way, plus a ReDoS safety rule. SDKs do not validate; the
|
|
12
|
+
* writers (this check, app-quonfig's copy, and the Git hook built from this
|
|
13
|
+
* repo) are the only gate.
|
|
14
|
+
*
|
|
15
|
+
* No imports: src/verify/ is bundled into the standalone qfg-verify hook.
|
|
16
|
+
*/
|
|
17
|
+
export const REGEX_V1_LIMITS = { maxLength: 256, maxRepeat: 1000, maxUnboundedQuantifiers: 2 };
|
|
18
|
+
/** The canonical docs section the error links to (qfg-ktkr.5 owns the page). */
|
|
19
|
+
export const REGEX_V1_DOCS_URL = 'https://docs.quonfig.com/docs/explanations/features/rules-and-segmentation#quonfig-regex-v1';
|
|
20
|
+
/** Fix-it hints, verbatim from the fixture's `hints` map. */
|
|
21
|
+
export const REGEX_V1_HINTS = {
|
|
22
|
+
'too-long': 'Regex patterns are limited to 256 characters.',
|
|
23
|
+
syntax: 'This is not a valid regular expression. Check for an unbalanced ( ) or [ ], a trailing \\, a reversed range such as [z-a], or a quantifier with nothing before it (a quantifier cannot follow ^, $, |, ( or another quantifier).',
|
|
24
|
+
lookaround: 'Lookahead and lookbehind ((?=, (?!, (?<=, (?<!) are not supported. Rewrite the pattern without them, or split the rule into two criteria.',
|
|
25
|
+
'named-group': "Named groups ((?<name>...), (?P<name>...), (?'name'...)) are not supported. Use a plain group ( ) or (?: ).",
|
|
26
|
+
'inline-flag': 'Inline flags such as (?i) are not supported. Spell out both cases with a class, for example [Hh]ello.',
|
|
27
|
+
'group-extension': 'Only ( ) and (?: ) groups are supported. Atomic groups (?>...), comments (?#...), conditionals and other (? forms are not.',
|
|
28
|
+
backref: 'Backreferences (\\1 to \\9, \\k<name>) are not supported. Repeat the text explicitly instead.',
|
|
29
|
+
octal: 'Octal escapes and \\0 are not supported. Write the character as \\xHH, for example \\x00.',
|
|
30
|
+
possessive: 'Possessive quantifiers (*+, ++, ?+, {n}+) are not supported. Use the plain form (a+) or the lazy form (a+?).',
|
|
31
|
+
'bad-repeat': 'Write repeat counts as {n}, {n,} or {n,m} with n <= m <= 1000. {,m} is not portable: write {0,m}. Escape a literal brace as \\{.',
|
|
32
|
+
'class-syntax': 'Inside [...] use only literals, ranges and escapes. POSIX classes such as [:digit:], a nested [, &&, --, ~~ and || are not supported. Escape a literal [ or ] as \\[ or \\], and do not start a class with ].',
|
|
33
|
+
'unicode-shorthand': '\\d, \\w, \\s, \\b and their negations match different characters in different SDKs. Use [0-9], [A-Za-z0-9_] or [ \\t\\r\\n\\f\\x0B].',
|
|
34
|
+
'unsupported-escape': 'This escape is not supported. The supported escapes are \\ followed by ASCII punctuation other than _, \\t \\n \\r \\f, and \\xHH with exactly two hex digits from \\x00 to \\x7F. Write _ unescaped instead of \\_, \\x0B instead of \\v, and a character above \\x7F as the character itself.',
|
|
35
|
+
'nested-quantifier': 'A quantifier cannot apply to a group that contains a quantifier or a |, as in (a+)+ or (a|b)*. Such patterns can take exponential time. Quantify the inner part only, or use a class: [ab]* instead of (a|b)*.',
|
|
36
|
+
'too-many-unbounded': 'A pattern can have at most 2 unbounded quantifiers (*, +, {n,}). Bound the extra ones with {n,m}, or split the rule into two criteria.',
|
|
37
|
+
};
|
|
38
|
+
// qfg-ktkr.7: no \_ (.NET throws), no \v (java reads a vertical-whitespace
|
|
39
|
+
// class), and \xHH only up to \x7F (ruby rejects \x80-\xFF).
|
|
40
|
+
const ASCII_PUNCT = '!"#$%&\'()*+,-./:;<=>?@[\\]^`{|}~';
|
|
41
|
+
const SIMPLE_ESCAPES = { f: 0x0c, n: 0x0a, r: 0x0d, t: 0x09 };
|
|
42
|
+
const MAX_HEX_ESCAPE = 0x7f;
|
|
43
|
+
const SHORTHANDS = 'dDwWsSbB';
|
|
44
|
+
const HEX = /^[\dA-Fa-f]{2}$/;
|
|
45
|
+
class Violation {
|
|
46
|
+
reason;
|
|
47
|
+
constructor(reason) {
|
|
48
|
+
this.reason = reason;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const fail = (reason) => {
|
|
52
|
+
throw new Violation(reason);
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Check one pattern against Quonfig regex v1. Returns null when the pattern is
|
|
56
|
+
* in the dialect, or the reason and fix-it hint for the first violation.
|
|
57
|
+
*
|
|
58
|
+
* Precedence (fixture header): too-long first; then one left-to-right scan
|
|
59
|
+
* reports the first offending construct (an unclosed ( or [ is found at the
|
|
60
|
+
* end); then too-many-unbounded, which needs the whole pattern.
|
|
61
|
+
*/
|
|
62
|
+
export function checkRegexV1(pattern) {
|
|
63
|
+
const cps = [...pattern];
|
|
64
|
+
try {
|
|
65
|
+
if (cps.length > REGEX_V1_LIMITS.maxLength)
|
|
66
|
+
fail('too-long');
|
|
67
|
+
const unbounded = scan(cps);
|
|
68
|
+
if (unbounded > REGEX_V1_LIMITS.maxUnboundedQuantifiers)
|
|
69
|
+
fail('too-many-unbounded');
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (error instanceof Violation)
|
|
74
|
+
return { hint: REGEX_V1_HINTS[error.reason], reason: error.reason };
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** Scan the pattern; returns the unbounded-quantifier count or throws a Violation. */
|
|
79
|
+
function scan(p) {
|
|
80
|
+
// One frame per open group: does it (transitively) contain a quantifier or |?
|
|
81
|
+
const groups = [];
|
|
82
|
+
let last = 'none';
|
|
83
|
+
let lastGroupRisky = false;
|
|
84
|
+
let unbounded = 0;
|
|
85
|
+
let i = 0;
|
|
86
|
+
const markRisky = () => {
|
|
87
|
+
if (groups.length > 0)
|
|
88
|
+
groups[groups.length - 1] = true;
|
|
89
|
+
};
|
|
90
|
+
/** At p[i] === '{': parse a repeat. Returns [end index, unbounded] or throws. */
|
|
91
|
+
const parseRepeat = () => {
|
|
92
|
+
let j = i + 1;
|
|
93
|
+
let n = '';
|
|
94
|
+
while (j < p.length && p[j] >= '0' && p[j] <= '9')
|
|
95
|
+
n += p[j++];
|
|
96
|
+
let m;
|
|
97
|
+
let comma = false;
|
|
98
|
+
if (p[j] === ',') {
|
|
99
|
+
comma = true;
|
|
100
|
+
j++;
|
|
101
|
+
m = '';
|
|
102
|
+
while (j < p.length && p[j] >= '0' && p[j] <= '9')
|
|
103
|
+
m += p[j++];
|
|
104
|
+
}
|
|
105
|
+
if (p[j] !== '}' || n === '')
|
|
106
|
+
fail('bad-repeat'); // {,m}, a bare {, x{foo}
|
|
107
|
+
const lo = Number(n);
|
|
108
|
+
const hi = comma && m !== '' ? Number(m) : undefined;
|
|
109
|
+
if (lo > REGEX_V1_LIMITS.maxRepeat || (hi !== undefined && (hi > REGEX_V1_LIMITS.maxRepeat || hi < lo)))
|
|
110
|
+
fail('bad-repeat');
|
|
111
|
+
return [j, comma && m === ''];
|
|
112
|
+
};
|
|
113
|
+
/** At p[i] === '\\': validate one escape, return its code point (or -1 for a non-literal). */
|
|
114
|
+
const parseEscape = () => {
|
|
115
|
+
const c = p[i + 1];
|
|
116
|
+
if (c === undefined)
|
|
117
|
+
fail('syntax');
|
|
118
|
+
if (c >= '1' && c <= '9')
|
|
119
|
+
fail('backref');
|
|
120
|
+
if (c === 'k')
|
|
121
|
+
fail('backref');
|
|
122
|
+
if (c === '0')
|
|
123
|
+
fail('octal');
|
|
124
|
+
if (SHORTHANDS.includes(c))
|
|
125
|
+
fail('unicode-shorthand');
|
|
126
|
+
if (c in SIMPLE_ESCAPES) {
|
|
127
|
+
i += 2;
|
|
128
|
+
return SIMPLE_ESCAPES[c];
|
|
129
|
+
}
|
|
130
|
+
if (c === 'x') {
|
|
131
|
+
const hex = (p[i + 2] ?? '') + (p[i + 3] ?? '');
|
|
132
|
+
if (!HEX.test(hex))
|
|
133
|
+
fail('unsupported-escape');
|
|
134
|
+
const cp = Number.parseInt(hex, 16);
|
|
135
|
+
if (cp > MAX_HEX_ESCAPE)
|
|
136
|
+
fail('unsupported-escape');
|
|
137
|
+
i += 4;
|
|
138
|
+
return cp;
|
|
139
|
+
}
|
|
140
|
+
if (c.length === 1 && ASCII_PUNCT.includes(c)) {
|
|
141
|
+
i += 2;
|
|
142
|
+
return c.codePointAt(0);
|
|
143
|
+
}
|
|
144
|
+
return fail('unsupported-escape');
|
|
145
|
+
};
|
|
146
|
+
const parseClass = () => {
|
|
147
|
+
// p[i] === '['
|
|
148
|
+
i++;
|
|
149
|
+
if (p[i] === '^')
|
|
150
|
+
i++;
|
|
151
|
+
if (p[i] === ']')
|
|
152
|
+
fail('class-syntax'); // []a] / [^]a] read differently in node
|
|
153
|
+
const start = i;
|
|
154
|
+
let prev; // code point of the previous single literal, for ranges
|
|
155
|
+
for (;;) {
|
|
156
|
+
const c = p[i];
|
|
157
|
+
if (c === undefined)
|
|
158
|
+
fail('syntax'); // unclosed [
|
|
159
|
+
if (c === ']') {
|
|
160
|
+
const body = p.slice(start, i);
|
|
161
|
+
if (body.length >= 2 && body[0] === ':' && body.at(-1) === ':')
|
|
162
|
+
fail('class-syntax'); // [:alpha:]
|
|
163
|
+
i++;
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (c === '[')
|
|
167
|
+
fail('class-syntax'); // nested [ and [[:x:]]
|
|
168
|
+
if ('&-~|'.includes(c) && p[i + 1] === c)
|
|
169
|
+
fail('class-syntax'); // && -- ~~ ||
|
|
170
|
+
if (c === '-' && prev !== undefined && p[i + 1] !== ']' && p[i + 1] !== undefined) {
|
|
171
|
+
i++;
|
|
172
|
+
if (p[i] === '[')
|
|
173
|
+
fail('class-syntax');
|
|
174
|
+
const hi = p[i] === '\\' ? parseEscape() : p[i++].codePointAt(0);
|
|
175
|
+
if (hi < prev)
|
|
176
|
+
fail('syntax'); // [z-a]
|
|
177
|
+
prev = undefined;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
prev = c === '\\' ? parseEscape() : p[i++].codePointAt(0);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
while (i < p.length) {
|
|
184
|
+
const c = p[i];
|
|
185
|
+
switch (c) {
|
|
186
|
+
case '\\': {
|
|
187
|
+
parseEscape();
|
|
188
|
+
last = 'atom';
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
case '[': {
|
|
192
|
+
parseClass();
|
|
193
|
+
last = 'atom';
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
case '(': {
|
|
197
|
+
if (p[i + 1] === '?') {
|
|
198
|
+
const a = p[i + 2];
|
|
199
|
+
const b = p[i + 3];
|
|
200
|
+
if (a === ':') {
|
|
201
|
+
i += 3;
|
|
202
|
+
}
|
|
203
|
+
else if (a === '=' || a === '!' || (a === '<' && (b === '=' || b === '!'))) {
|
|
204
|
+
fail('lookaround');
|
|
205
|
+
}
|
|
206
|
+
else if (a === '<' || a === 'P' || a === "'") {
|
|
207
|
+
fail('named-group');
|
|
208
|
+
}
|
|
209
|
+
else if (a !== undefined && /[A-Za-z-]/.test(a)) {
|
|
210
|
+
fail('inline-flag');
|
|
211
|
+
}
|
|
212
|
+
else if (a === undefined) {
|
|
213
|
+
fail('syntax');
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
fail('group-extension');
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
i++;
|
|
221
|
+
}
|
|
222
|
+
groups.push(false);
|
|
223
|
+
last = 'none';
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
case ')': {
|
|
227
|
+
if (groups.length === 0)
|
|
228
|
+
fail('syntax');
|
|
229
|
+
lastGroupRisky = groups.pop();
|
|
230
|
+
if (lastGroupRisky)
|
|
231
|
+
markRisky(); // "contains" is transitive
|
|
232
|
+
last = 'group';
|
|
233
|
+
i++;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
case '|': {
|
|
237
|
+
markRisky();
|
|
238
|
+
last = 'none';
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
case '^':
|
|
242
|
+
case '$': {
|
|
243
|
+
last = 'none';
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
case '*':
|
|
247
|
+
case '+':
|
|
248
|
+
case '?':
|
|
249
|
+
case '{': {
|
|
250
|
+
let end = i;
|
|
251
|
+
let isUnbounded = c === '*' || c === '+';
|
|
252
|
+
if (c === '{')
|
|
253
|
+
[end, isUnbounded] = parseRepeat();
|
|
254
|
+
if (last === 'none' || last === 'quantifier')
|
|
255
|
+
fail('syntax');
|
|
256
|
+
if (last === 'group' && lastGroupRisky)
|
|
257
|
+
fail('nested-quantifier');
|
|
258
|
+
markRisky();
|
|
259
|
+
if (isUnbounded)
|
|
260
|
+
unbounded++;
|
|
261
|
+
i = end + 1;
|
|
262
|
+
if (p[i] === '?')
|
|
263
|
+
i++; // lazy
|
|
264
|
+
else if (p[i] === '+')
|
|
265
|
+
fail('possessive');
|
|
266
|
+
last = 'quantifier';
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
default: {
|
|
270
|
+
last = 'atom';
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
i++;
|
|
274
|
+
}
|
|
275
|
+
if (groups.length > 0)
|
|
276
|
+
fail('syntax'); // unclosed (
|
|
277
|
+
return unbounded;
|
|
278
|
+
}
|
|
@@ -35,7 +35,9 @@ export declare function hookVersion(env?: HookEnv): string;
|
|
|
35
35
|
* old oid that is an ancestor of the new oid. A delete and a create are both
|
|
36
36
|
* rejected (provisioning creates `main` with auto_init; no writer creates it by
|
|
37
37
|
* push, and the zero-oid door is the only way to delete-then-recreate).
|
|
38
|
-
* Other refs are untouched.
|
|
38
|
+
* Other refs are untouched. Enforced on every repo, unconditionally: the
|
|
39
|
+
* QUONFIG_HOOK_FF_ENFORCE rollout switch is gone (qfg-jxml.29), so no env var
|
|
40
|
+
* can turn the rule off. See plan 2026-09-17-tree-derived-cache 5.6.
|
|
39
41
|
*/
|
|
40
42
|
export declare function checkAppendOnlyMain(refs: readonly RefUpdate[], env: HookEnv, cwd?: string): {
|
|
41
43
|
bypassReason?: string;
|
|
@@ -127,11 +127,13 @@ const REWRITE_OPTION = 'quonfig-rewrite=';
|
|
|
127
127
|
* old oid that is an ancestor of the new oid. A delete and a create are both
|
|
128
128
|
* rejected (provisioning creates `main` with auto_init; no writer creates it by
|
|
129
129
|
* push, and the zero-oid door is the only way to delete-then-recreate).
|
|
130
|
-
* Other refs are untouched.
|
|
130
|
+
* Other refs are untouched. Enforced on every repo, unconditionally: the
|
|
131
|
+
* QUONFIG_HOOK_FF_ENFORCE rollout switch is gone (qfg-jxml.29), so no env var
|
|
132
|
+
* can turn the rule off. See plan 2026-09-17-tree-derived-cache 5.6.
|
|
131
133
|
*/
|
|
132
134
|
export function checkAppendOnlyMain(refs, env, cwd) {
|
|
133
135
|
const mainUpdates = refs.filter((ref) => ref.refName === MAIN_REF);
|
|
134
|
-
if (mainUpdates.length === 0
|
|
136
|
+
if (mainUpdates.length === 0)
|
|
135
137
|
return { errors: [] };
|
|
136
138
|
const bypassReason = operatorBypassReason(env);
|
|
137
139
|
if (bypassReason)
|
|
@@ -159,23 +161,6 @@ export function checkAppendOnlyMain(refs, env, cwd) {
|
|
|
159
161
|
}
|
|
160
162
|
return { errors };
|
|
161
163
|
}
|
|
162
|
-
/** Rollout switch: enforce only for repos named in the allowlist; `*` = all. */
|
|
163
|
-
function ffEnforcedForRepo(env) {
|
|
164
|
-
const allowlist = (env.QUONFIG_HOOK_FF_ENFORCE ?? '')
|
|
165
|
-
.split(',')
|
|
166
|
-
.map((entry) => entry.trim().toLowerCase())
|
|
167
|
-
.filter(Boolean);
|
|
168
|
-
if (allowlist.length === 0)
|
|
169
|
-
return false;
|
|
170
|
-
if (allowlist.includes('*'))
|
|
171
|
-
return true;
|
|
172
|
-
// Gitea 1.25 hands the hook its repo via these (modules/repository/env.go).
|
|
173
|
-
const owner = (env.GITEA_REPO_USER_NAME ?? '').trim().toLowerCase();
|
|
174
|
-
const name = (env.GITEA_REPO_NAME ?? '').trim().toLowerCase();
|
|
175
|
-
if (!owner || !name)
|
|
176
|
-
return false;
|
|
177
|
-
return allowlist.includes(`${owner}/${name}`);
|
|
178
|
-
}
|
|
179
164
|
/**
|
|
180
165
|
* Operator bypass, two keys, BOTH required: the pusher is the operator admin
|
|
181
166
|
* account (the app pushes as admin too, so identity only excludes customers)
|
package/dist/verify/validate.js
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
import { z } from 'zod';
|
|
29
29
|
import { findMapKeyOrderWarnings, navigate, scanOrderedJson } from './map-key-order.js';
|
|
30
30
|
import { compileSchema, validateAgainstSchema } from './schema-validator.js';
|
|
31
|
+
import { checkRegexV1, REGEX_V1_DOCS_URL } from './regex-dialect.js';
|
|
31
32
|
import { DURATION_FORMAT_HINT, INT_RANGE_FORMAT_HINT, isValidIntRange, isValidIsoDuration, isValidSemver, isValidTimestamp, parseDoubleValue, parseIntValue, SEMVER_FORMAT_HINT, TIMESTAMP_FORMAT_HINT, } from './value-grammar.js';
|
|
32
33
|
// Inlined to keep this directory self-contained for the standalone bun-compile
|
|
33
34
|
// build that runs as the qfg-verify pre-receive hook in app-gitea — that build
|
|
@@ -1341,6 +1342,25 @@ function criterionValueShapeIssue(criterion) {
|
|
|
1341
1342
|
return { hint: SEMVER_FORMAT_HINT, problem: `needs a version string; got ${vtm.type} ${shown}` };
|
|
1342
1343
|
return undefined;
|
|
1343
1344
|
}
|
|
1345
|
+
case 'PROP_DOES_NOT_MATCH':
|
|
1346
|
+
case 'PROP_MATCHES': {
|
|
1347
|
+
// Quonfig regex v1 (qfg-ktkr.2): an out-of-dialect pattern compiles in
|
|
1348
|
+
// some SDKs and not others (or runs in exponential time), so the rule
|
|
1349
|
+
// silently diverges across SDKs. One check here covers qfg verify, the
|
|
1350
|
+
// Git hook and both push engines.
|
|
1351
|
+
if (vtm.type !== 'string' || typeof vtm.value !== 'string')
|
|
1352
|
+
return {
|
|
1353
|
+
hint: `Store the pattern as {"type": "string", "value": "<pattern>"}. See ${REGEX_V1_DOCS_URL}`,
|
|
1354
|
+
problem: `needs a string pattern; got ${vtm.type} ${shown}`,
|
|
1355
|
+
};
|
|
1356
|
+
const violation = checkRegexV1(vtm.value);
|
|
1357
|
+
if (violation)
|
|
1358
|
+
return {
|
|
1359
|
+
hint: `${violation.hint} See ${REGEX_V1_DOCS_URL}`,
|
|
1360
|
+
problem: `pattern ${shown} is outside Quonfig regex v1 (${violation.reason})`,
|
|
1361
|
+
};
|
|
1362
|
+
return undefined;
|
|
1363
|
+
}
|
|
1344
1364
|
default: {
|
|
1345
1365
|
return undefined;
|
|
1346
1366
|
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default: "0.
|
|
1
|
+
declare const _default: "0.5.0";
|
|
2
2
|
export default _default;
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// AUTO-GENERATED from package.json by scripts/generate-version.mjs — do not edit.
|
|
2
|
-
export default '0.
|
|
2
|
+
export default '0.5.0';
|