@everystack/cli 0.4.67 → 0.4.68

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@everystack/cli",
3
- "version": "0.4.67",
3
+ "version": "0.4.68",
4
4
  "description": "CLI and OTA updates for Expo apps on everystack",
5
5
  "license": "AGPL-3.0-only",
6
6
  "author": "Scalable Technology, Inc. <licensing@scalable.technology>",
@@ -24,13 +24,33 @@
24
24
  * for n ≥ 1. PostgreSQL defines an array-to-array cast element-wise (parse_coerce), so
25
25
  * the two expressions denote the same value for every input; wrapping an element in
26
26
  * parentheses is parse-neutral. The empty array is EXCLUDED: `ARRAY[]` without a cast has
27
- * no type, so its cast is load-bearing and stays.
27
+ * no type, so its cast is load-bearing and stays. (The measured CROSS-VERSION case: two PG
28
+ * majors const-fold the same DDL differently.)
29
+ *
30
+ * LITERAL-TEXT-CAST STRIP: `'lit'::text == 'lit'`, but ONLY the bare `text` cast applied
31
+ * directly to a single-quoted string literal. This is a CROSS-PRODUCER class, present on
32
+ * every version: PostgreSQL coerces an unknown-type string literal compared to a text
33
+ * column and RECORDS `'lit'::text` in the stored tree, while the author's model source
34
+ * writes the un-cast `'lit'`. The two producers' texts never converge without this rule, so
35
+ * a `text_col = 'lit'` partial index / CHECK / policy re-diffs DROP+CREATE forever and the
36
+ * fingerprint gate cannot MATCH. Safe because `text` takes no typmod and its input function
37
+ * is the identity on the literal's bytes — the cast cannot change the value. The rule is
38
+ * `text`-ONLY for exactly that reason: `bpchar`/`character(n)` blank-pad or blank-trim,
39
+ * `varchar(n)`/`"char"` truncate — those casts CHANGE the value, so stripping one would
40
+ * collapse two opposite-truth predicates to equal text (a false match, the disaster). The
41
+ * array-type cast `::text[]` is likewise excluded — it carries the element type. See the
42
+ * `char(2)` false-match test for why the family is not widened.
28
43
  *
29
44
  * Anything the scanner does not positively recognize is left byte-for-byte unchanged — an
30
45
  * unrecognized spelling is a MISS (drop + create, the pre-existing behavior), never a guess.
31
- * Escalation is named, not implied: the SECOND cross-version variant found in the field is
46
+ * Escalation is named, not implied: the SECOND cross-VERSION variant found in the field is
32
47
  * the trigger to adopt libpg_query and move identity onto normalized ASTs (Ty, 2026-08-09) —
33
- * a growing rule table over raw text is where scanning stops being defensible.
48
+ * a growing rule table over raw text is where scanning stops being defensible. One correction
49
+ * to that trigger (2026-08-24): libpg_query closes cross-VERSION SYNTAX drift (whitespace,
50
+ * parens, keyword case), NOT cross-PRODUCER COERCION drift — a raw parse tree carries no type
51
+ * resolution, so `'lit'` and `'lit'::text` still differ structurally without a catalog, which
52
+ * the CI-pure `db:diff` lane does not have. That class earns a provable text rewrite here,
53
+ * verified end-to-end by `db:check`'s from-scratch build on a real database.
34
54
  *
35
55
  * Applied at the PRODUCERS (live introspection's `predicate()`, the compiler's policy
36
56
  * assembly), so every downstream comparison — the identity matcher, the canonical hash, the
@@ -121,6 +141,53 @@ function distributeOnce(s: string): string | null {
121
141
  return null;
122
142
  }
123
143
 
144
+ /**
145
+ * A bare `text` cast token — `::text` or `::pg_catalog.text` — from just past a literal's
146
+ * closing quote. Whitespace around `::` is tolerated (pg emits none; an author might). The
147
+ * trailing `\b` rejects `::textfoo`; the caller additionally rejects a following `[` so the
148
+ * array-type cast `::text[]` is never stripped.
149
+ */
150
+ const LITERAL_TEXT_CAST = /^\s*::\s*(?:pg_catalog\.)?text\b/;
151
+
152
+ /**
153
+ * Strip a bare `text` cast applied directly to a single-quoted string literal, wherever one
154
+ * appears. One left-to-right pass is complete: removing a cast token cannot create a new
155
+ * `'literal'::text` pair to its left, and it never forms an `(ARRAY[…])::T[]` for the other
156
+ * rule. Quoted regions are skipped so a `::text` inside a literal is content, not structure.
157
+ */
158
+ function stripLiteralTextCast(s: string): string {
159
+ let out = '';
160
+ let i = 0;
161
+ while (i < s.length) {
162
+ const c = s[i];
163
+ if (c === "'") {
164
+ const close = skipQuoted(s, i);
165
+ if (close < 0) return out + s.slice(i); // unterminated literal — leave the rest alone
166
+ out += s.slice(i, close + 1); // the literal itself, verbatim
167
+ i = close + 1;
168
+ // Drop a bare text cast on the literal, and any stacked `::text::text`, so the result
169
+ // is a true fixpoint. `::text[]` is the array-type cast (element type, load-bearing),
170
+ // not a scalar text cast — the look-ahead skips whitespace so `::text []` is kept too.
171
+ let m = LITERAL_TEXT_CAST.exec(s.slice(i));
172
+ while (m && !/^\s*\[/.test(s.slice(i + m[0].length))) {
173
+ i += m[0].length; // drop the cast token
174
+ m = LITERAL_TEXT_CAST.exec(s.slice(i));
175
+ }
176
+ continue;
177
+ }
178
+ if (c === '"') {
179
+ const close = skipQuoted(s, i);
180
+ if (close < 0) return out + s.slice(i);
181
+ out += s.slice(i, close + 1); // a quoted identifier — never a string literal, skip whole
182
+ i = close + 1;
183
+ continue;
184
+ }
185
+ out += c;
186
+ i++;
187
+ }
188
+ return out;
189
+ }
190
+
124
191
  /**
125
192
  * Normalize one deparsed expression. Idempotent; unrecognized text returns unchanged.
126
193
  * Fixpoint-bounded: nesting deeper than the cap returns its progress, which is still
@@ -130,8 +197,8 @@ export function normalizeDeparsedExpr(text: string): string {
130
197
  let s = text;
131
198
  for (let n = 0; n < 64; n++) {
132
199
  const next = distributeOnce(s);
133
- if (next === null) return s;
200
+ if (next === null) break;
134
201
  s = next;
135
202
  }
136
- return s;
203
+ return stripLiteralTextCast(s);
137
204
  }