@tailor-platform/sdk-codemod 0.6.0 → 0.8.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/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # @tailor-platform/sdk-codemod
2
2
 
3
+ ## 0.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#2136](https://github.com/tailor-platform/sdk/pull/2136) [`6fba096`](https://github.com/tailor-platform/sdk/commit/6fba09676fc20e08e3325c26c0e72dc9ed4fd8f6) Thanks [@toiroakr](https://github.com/toiroakr)! - `.relation()`'s `toward.type` option is renamed to `toward.table`, since it names a target table rather than a TypeScript/GraphQL type — matching the `db.type()` → `db.table()` rename. The old spelling keeps working as a deprecated alias until v3; `tailor upgrade` offers the `v3/relation-toward-table` codemod to rewrite `toward: { type: ... }` to `toward: { table: ... }` across TypeScript/JavaScript sources. The relation's own `type` (its cardinality, e.g. `"n-1"`) is unchanged.
8
+
9
+ ### Patch Changes
10
+
11
+ - [#2135](https://github.com/tailor-platform/sdk/pull/2135) [`5b7b676`](https://github.com/tailor-platform/sdk/commit/5b7b676740350dfe35ae479aa73da1f67c4d4f2f) Thanks [@renovate](https://github.com/apps/renovate)! - fix(deps): update dependency politty to v0.11.9
12
+
13
+ ## 0.7.0
14
+
15
+ ### Minor Changes
16
+
17
+ - [#2088](https://github.com/tailor-platform/sdk/pull/2088) [`c9f91d9`](https://github.com/tailor-platform/sdk/commit/c9f91d944e4b041250979ec4438c36cabf818e14) Thanks [@dqn](https://github.com/dqn)! - The `--branch` option of `tailor setup branch` is renamed to `--target`, so it no longer collides with the subcommand name. The old spelling keeps working as a deprecated alias until v3 and prints a deprecation warning when used; `tailor upgrade` offers the `v3/setup-branch-flag-rename` codemod to rewrite `setup branch --branch` invocations across package.json scripts, shell and Windows scripts, YAML, Markdown, and JavaScript/TypeScript sources. The `--branch` option of `setup tag`, `setup preview`, and `setup coordinate` is unchanged.
18
+
19
+ ### Patch Changes
20
+
21
+ - [#2137](https://github.com/tailor-platform/sdk/pull/2137) [`d38497a`](https://github.com/tailor-platform/sdk/commit/d38497ac214f547483028e2622d53dbf1414ecb2) Thanks [@dqn](https://github.com/dqn)! - Keep LLM review detection working in multi-major upgrades: each codemod's review detector and suspicious patterns now inspect the file as of that codemod's position in the transform chain, so a later codemod's rewrite can no longer silently hide an earlier codemod's review findings.
22
+
23
+ - [#2138](https://github.com/tailor-platform/sdk/pull/2138) [`870c8cd`](https://github.com/tailor-platform/sdk/commit/870c8cdaa007adcbb8f565fe9b4d238d98c00e6d) Thanks [@dqn](https://github.com/dqn)! - Stop exporting internal declarations that were only used within their own module.
24
+
25
+ - [#2089](https://github.com/tailor-platform/sdk/pull/2089) [`90948e6`](https://github.com/tailor-platform/sdk/commit/90948e6f6e1f3624a9c30595b3ff3a46ffbbced0) Thanks [@toiroakr](https://github.com/toiroakr)! - Add a targeted hint to the `Remote schema drift detected` error when every reported drift is a missing script hash — the pattern left by an environment last deployed with the pre-v2 CLI, which never wrote script hashes. The hint points at `migration sync <N>`, which is already listed as one of the general resolution options. The v2 migration guide (`docs/migration/v2.md`) now also documents that the first `tailor deploy` against such an environment needs a `migration sync` first.
26
+
3
27
  ## 0.6.0
4
28
 
5
29
  ### Minor Changes
@@ -0,0 +1,233 @@
1
+ import { l as stringValue } from "../../../ast-grep-helpers-CXtWn3RB.js";
2
+ import { Lang, parse } from "@ast-grep/napi";
3
+ //#region codemods/v3/relation-toward-table/scripts/transform.ts
4
+ const LEGACY_KEY = "type";
5
+ const NEW_KEY = "table";
6
+ function sourceLang(filePath, source) {
7
+ const lowerPath = filePath.toLowerCase();
8
+ if (/\.(?:ts|mts|cts)$/u.test(lowerPath)) return Lang.TypeScript;
9
+ if (/\.(?:tsx|jsx|js)$/u.test(lowerPath)) return Lang.Tsx;
10
+ return source.includes("</") ? Lang.Tsx : Lang.TypeScript;
11
+ }
12
+ function relationBindingName(pattern) {
13
+ if (pattern.kind() !== "object_pattern") return null;
14
+ for (const child of pattern.children()) {
15
+ if (child.kind() === "shorthand_property_identifier_pattern" && child.text() === "relation") return child.text();
16
+ if (child.kind() === "pair_pattern" && stringValue(child.field("key")) === "relation") {
17
+ const value = child.field("value");
18
+ return value?.kind() === "identifier" ? value.text() : null;
19
+ }
20
+ if (child.kind() === "object_assignment_pattern") {
21
+ const binding = child.children().find((node) => node.kind() === "shorthand_property_identifier_pattern");
22
+ if (binding?.text() === "relation") return binding.text();
23
+ }
24
+ }
25
+ return null;
26
+ }
27
+ function relationAliases(root) {
28
+ const aliases = /* @__PURE__ */ new Set();
29
+ for (const pattern of root.findAll({ rule: { kind: "object_pattern" } })) {
30
+ const name = relationBindingName(pattern);
31
+ if (name) aliases.add(name);
32
+ }
33
+ return aliases;
34
+ }
35
+ function isRelationCall(call, aliases) {
36
+ const callee = call.children()[0];
37
+ if (!callee) return false;
38
+ if (callee.kind() === "identifier") return aliases.has(callee.text());
39
+ if (callee.kind() === "subscript_expression") {
40
+ const property = literalStringValue(callee.field("index"));
41
+ return property === null || property === "relation";
42
+ }
43
+ if (callee.kind() !== "member_expression") return false;
44
+ return callee.children().findLast((child) => child.kind() === "property_identifier" || child.kind() === "identifier")?.text() === "relation";
45
+ }
46
+ function callArgument(call) {
47
+ const args = call.children().find((child) => child.kind() === "arguments");
48
+ if (!args) return null;
49
+ const values = args.children().filter((child) => {
50
+ const kind = child.kind();
51
+ return kind !== "(" && kind !== ")" && kind !== "," && kind !== "comment";
52
+ });
53
+ return values.length === 1 ? values[0] : null;
54
+ }
55
+ function pairKey(pair) {
56
+ const key = pair.children()[0];
57
+ return stringValue(key ?? null);
58
+ }
59
+ function pairValue(pair) {
60
+ const children = pair.children();
61
+ const colonIndex = children.findIndex((child) => child.kind() === ":");
62
+ if (colonIndex === -1) return null;
63
+ return children.slice(colonIndex + 1).find((child) => child.kind() !== "comment") ?? null;
64
+ }
65
+ function objectPair(object, key) {
66
+ return object.children().find((child) => child.kind() === "pair" && pairKey(child) === key) ?? null;
67
+ }
68
+ function literalStringValue(node) {
69
+ if (node?.kind() !== "string") return null;
70
+ return stringValue(node);
71
+ }
72
+ function hasDynamicProperties(object) {
73
+ return object.children().some((child) => {
74
+ const kind = child.kind();
75
+ if (kind === "{" || kind === "}" || kind === "," || kind === "comment") return false;
76
+ if (kind !== "pair") return true;
77
+ const keyKind = child.children()[0]?.kind();
78
+ return keyKind !== "property_identifier" && keyKind !== "string";
79
+ });
80
+ }
81
+ /**
82
+ * Same as {@link hasDynamicProperties}, but for a `toward` object
83
+ * specifically: a bare `{ type }` shorthand is a safe, rewritable spelling
84
+ * (only the key moves, matching `renameEdit`'s shorthand handling), not a
85
+ * sign of an unsafe/computed key.
86
+ */
87
+ function hasUnsafeTowardProperties(towardObject) {
88
+ return towardObject.children().some((child) => {
89
+ const kind = child.kind();
90
+ if (kind === "{" || kind === "}" || kind === "," || kind === "comment") return false;
91
+ if (kind === "shorthand_property_identifier") return false;
92
+ if (kind !== "pair") return true;
93
+ const keyKind = child.children()[0]?.kind();
94
+ return keyKind !== "property_identifier" && keyKind !== "string";
95
+ });
96
+ }
97
+ function findLegacyEntry(towardObject) {
98
+ for (const child of towardObject.children()) {
99
+ if (child.kind() === "shorthand_property_identifier" && child.text() === LEGACY_KEY) return {
100
+ node: child,
101
+ shorthand: true
102
+ };
103
+ if (child.kind() !== "pair") continue;
104
+ const key = child.children()[0];
105
+ if (!key) continue;
106
+ if (key.kind() !== "property_identifier" && key.kind() !== "string") continue;
107
+ if (stringValue(key) !== LEGACY_KEY) continue;
108
+ return {
109
+ node: key,
110
+ shorthand: false
111
+ };
112
+ }
113
+ return null;
114
+ }
115
+ function renameEdit(entry) {
116
+ if (entry.shorthand) return entry.node.replace(`${NEW_KEY}: ${LEGACY_KEY}`);
117
+ const text = entry.node.text();
118
+ if (entry.node.kind() !== "string") return entry.node.replace(NEW_KEY);
119
+ const quote = text.startsWith("'") ? "'" : text.startsWith("`") ? "`" : "\"";
120
+ return entry.node.replace(`${quote}${NEW_KEY}${quote}`);
121
+ }
122
+ function parseRoot(source, filePath) {
123
+ try {
124
+ return parse(sourceLang(filePath, source), source).root();
125
+ } catch {
126
+ return null;
127
+ }
128
+ }
129
+ /**
130
+ * Rename `.relation()`'s `toward.type` option to `toward.table`.
131
+ * @param source - File contents
132
+ * @param filePath - Path to the file being transformed
133
+ * @returns Transformed source, or null when nothing matched
134
+ */
135
+ function transform(source, filePath = "") {
136
+ if (!source.includes("relation")) return null;
137
+ const root = parseRoot(source, filePath);
138
+ if (!root) return null;
139
+ const aliases = relationAliases(root);
140
+ const edits = [];
141
+ for (const call of root.findAll({ rule: { kind: "call_expression" } })) {
142
+ if (!isRelationCall(call, aliases)) continue;
143
+ const config = callArgument(call);
144
+ if (config?.kind() !== "object") continue;
145
+ if (hasDynamicProperties(config)) continue;
146
+ if (!objectPair(config, "type")) continue;
147
+ const toward = objectPair(config, "toward");
148
+ if (!toward) continue;
149
+ const towardConfig = pairValue(toward);
150
+ if (towardConfig?.kind() !== "object") continue;
151
+ if (hasUnsafeTowardProperties(towardConfig)) continue;
152
+ if (objectPair(towardConfig, NEW_KEY)) continue;
153
+ const entry = findLegacyEntry(towardConfig);
154
+ if (!entry) continue;
155
+ edits.push(renameEdit(entry));
156
+ }
157
+ return edits.length > 0 ? root.commitEdits(edits) : null;
158
+ }
159
+ function lineOf(node) {
160
+ return node.range().start.line + 1;
161
+ }
162
+ function excerptOf(node) {
163
+ return node.text().split("\n", 1)[0].trim();
164
+ }
165
+ /**
166
+ * Report `.relation()` calls this transform cannot safely rewrite: a
167
+ * non-object call argument, a computed/spread key on the config or `toward`
168
+ * object, or a `toward` reached through something other than a literal
169
+ * object (e.g. a shared variable).
170
+ * @param source - File contents
171
+ * @param filePath - Path to the file being reviewed
172
+ * @param relativePath - Repository-relative path reported to the user
173
+ * @returns Findings for occurrences needing a manual rename
174
+ */
175
+ function reviewFindings(source, filePath, relativePath) {
176
+ if (!source.includes("relation")) return [];
177
+ const root = parseRoot(source, filePath);
178
+ if (!root) return [];
179
+ const aliases = relationAliases(root);
180
+ const findings = [];
181
+ for (const call of root.findAll({ rule: { kind: "call_expression" } })) {
182
+ if (!isRelationCall(call, aliases)) continue;
183
+ const config = callArgument(call);
184
+ if (config?.kind() !== "object") {
185
+ if (config) findings.push({
186
+ file: relativePath,
187
+ line: lineOf(call),
188
+ message: "This .relation() call's config isn't a literal object; check its toward.type manually.",
189
+ excerpt: excerptOf(call)
190
+ });
191
+ continue;
192
+ }
193
+ if (hasDynamicProperties(config)) {
194
+ findings.push({
195
+ file: relativePath,
196
+ line: lineOf(config),
197
+ message: "A computed/spread key on this .relation() config may hide toward.type.",
198
+ excerpt: excerptOf(config)
199
+ });
200
+ continue;
201
+ }
202
+ const toward = objectPair(config, "toward");
203
+ if (!toward) continue;
204
+ const towardConfig = pairValue(toward);
205
+ if (towardConfig?.kind() !== "object") {
206
+ findings.push({
207
+ file: relativePath,
208
+ line: lineOf(toward),
209
+ message: "This .relation() call's toward isn't a literal object; rename type to table by hand.",
210
+ excerpt: excerptOf(toward)
211
+ });
212
+ continue;
213
+ }
214
+ if (hasUnsafeTowardProperties(towardConfig)) {
215
+ findings.push({
216
+ file: relativePath,
217
+ line: lineOf(towardConfig),
218
+ message: "A computed/spread key on this toward object may hide type.",
219
+ excerpt: excerptOf(towardConfig)
220
+ });
221
+ continue;
222
+ }
223
+ if (objectPair(towardConfig, NEW_KEY) && findLegacyEntry(towardConfig)) findings.push({
224
+ file: relativePath,
225
+ line: lineOf(towardConfig),
226
+ message: "This toward object has both table and type; remove the deprecated type by hand instead of automatically renaming it (would produce a duplicate key).",
227
+ excerpt: excerptOf(towardConfig)
228
+ });
229
+ }
230
+ return findings;
231
+ }
232
+ //#endregion
233
+ export { transform as default, reviewFindings };
@@ -0,0 +1,76 @@
1
+ import * as path from "pathe";
2
+ //#region codemods/v3/setup-branch-flag-rename/scripts/transform.ts
3
+ const TOKEN_SEPARATOR = "(?:[ \\t]|\\\\\\r?\\n)+";
4
+ const COMMAND_START = new RegExp(`(?<![\\w.-])tailor(?:-sdk)?(?:\\.(?:cmd|ps1|exe))?${TOKEN_SEPARATOR}setup${TOKEN_SEPARATOR}branch(?![\\w.-])`, "g");
5
+ const NEXT_TOKEN = new RegExp(`${TOKEN_SEPARATOR}("[^"\\n]*"|'[^'\\n]*'|[^\\s;&|]+)`, "y");
6
+ function replaceCommand(value) {
7
+ let result = "";
8
+ let cursor = 0;
9
+ COMMAND_START.lastIndex = 0;
10
+ for (let match = COMMAND_START.exec(value); match; match = COMMAND_START.exec(value)) {
11
+ let pos = COMMAND_START.lastIndex;
12
+ result += value.slice(cursor, pos);
13
+ NEXT_TOKEN.lastIndex = pos;
14
+ for (let token = NEXT_TOKEN.exec(value); token; token = NEXT_TOKEN.exec(value)) {
15
+ const chunk = token[0];
16
+ const word = token[1];
17
+ if (word.startsWith("#")) break;
18
+ const quoted = /^(['"])(--branch(?:=.*)?)\1$/.exec(word);
19
+ const quote = quoted ? quoted[1] : "";
20
+ const bare = quoted ? quoted[2] : word;
21
+ if (bare === "--branch" || bare.startsWith("--branch=")) {
22
+ const separator = chunk.slice(0, chunk.length - word.length);
23
+ result += `${separator}${quote}--target${bare.slice(8)}${quote}`;
24
+ } else result += chunk;
25
+ pos = NEXT_TOKEN.lastIndex;
26
+ if (/\$\(|`|<\(|>\(/.test(word)) break;
27
+ }
28
+ cursor = pos;
29
+ COMMAND_START.lastIndex = pos;
30
+ }
31
+ result += value.slice(cursor);
32
+ return result;
33
+ }
34
+ function transformText(source) {
35
+ const updated = replaceCommand(source);
36
+ return updated === source ? null : updated;
37
+ }
38
+ function transformPackageJson(source) {
39
+ let parsed;
40
+ try {
41
+ parsed = JSON.parse(source);
42
+ } catch {
43
+ return null;
44
+ }
45
+ let modified = false;
46
+ const scripts = parsed.scripts;
47
+ if (typeof scripts === "object" && scripts != null && !Array.isArray(scripts)) for (const [name, value] of Object.entries(scripts)) {
48
+ if (typeof value !== "string") continue;
49
+ const updated = replaceCommand(value);
50
+ if (updated !== value) {
51
+ scripts[name] = updated;
52
+ modified = true;
53
+ }
54
+ }
55
+ if (!modified) return null;
56
+ const trailing = source.endsWith("\n") ? "\n" : "";
57
+ return JSON.stringify(parsed, null, 2) + trailing;
58
+ }
59
+ /**
60
+ * Rename the `--branch` option of `tailor setup branch` invocations to
61
+ * `--target`.
62
+ *
63
+ * `--branch` remains as a deprecated alias until v3, where only
64
+ * `--target` is recognized. `setup tag`, `setup preview`, and
65
+ * `setup coordinate` keep their own `--branch` option unchanged.
66
+ * @param source - File contents
67
+ * @param filePath - Absolute path to the file (used to dispatch package.json vs text)
68
+ * @returns Transformed source or null when nothing matched.
69
+ */
70
+ function transform(source, filePath) {
71
+ if (!source.includes("--branch")) return null;
72
+ if (path.extname(filePath).toLowerCase() === ".json") return transformPackageJson(source);
73
+ return transformText(source);
74
+ }
75
+ //#endregion
76
+ export { transform as default };
package/dist/index.js CHANGED
@@ -123,6 +123,7 @@ const V2_NEXT_6 = "2.0.0-next.6";
123
123
  const V2_NEXT_7 = "2.0.0-next.7";
124
124
  const V2_NEXT_9 = "2.0.0-next.9";
125
125
  const V2_NEXT_11 = "2.0.0-next.11";
126
+ const SETUP_BRANCH_RESIDUAL_FLAG = /(?:(?<![\w.-])tailor(?:-sdk)?(?:\.(?:cmd|ps1|exe))?|@tailor-platform\/sdk(?:@\S{1,32})?)[\s\\^`]{1,16}setup[\s\\^`]{1,16}branch\b(?:'[^'\n]*'|"[^"\n]*"|[^\n;&|'"#]|[\\^`]\r?\n)*(?:[ \t]|[\\^`]\r?\n)--branch(?![\w-])/;
126
127
  /** All registered codemods, in registration order. */
127
128
  const allCodemods = [
128
129
  {
@@ -671,7 +672,7 @@ const allCodemods = [
671
672
  after: [
672
673
  "ownerId: db.uuid().relation({",
673
674
  " type: \"n-1\",",
674
- " toward: { type: user, as: \"user\" },",
675
+ " toward: { table: user, as: \"user\" },",
675
676
  "}),"
676
677
  ].join("\n")
677
678
  }],
@@ -816,7 +817,11 @@ const allCodemods = [
816
817
  " them to StartWorkflowOptions / ExecJobFunctionOptions.",
817
818
  "- An invoker option passed via a variable or spread (not a literal object) —",
818
819
  " the codemod only inspects literal object arguments; rename the invoker key",
819
- " to authInvoker in the options object's own definition."
820
+ " to authInvoker in the options object's own definition.",
821
+ "- A renamed triggerJobFunction call whose target is another workflow job —",
822
+ " rewrite it further to that job's own .start() method (e.g. worker.start(args)).",
823
+ " Calling execJobFunction directly is not detected as a build-time dependency",
824
+ " and fails the build."
820
825
  ].join("\n")
821
826
  },
822
827
  {
@@ -849,7 +854,45 @@ const allCodemods = [
849
854
  "- mockWorkflow().startJobFunction in tests — assert on the execJobFunction vi.fn",
850
855
  " instead; the alias was the same mock function.",
851
856
  "- A file that already imports ExecJobFunctionOptions alongside the removed type —",
852
- " rename the remaining references by hand and drop the duplicate specifier."
857
+ " rename the remaining references by hand and drop the duplicate specifier.",
858
+ "- A renamed call whose target is another workflow job — rewrite it further to",
859
+ " that job's own .start() method (e.g. worker.start(args)). Calling",
860
+ " execJobFunction directly is not detected as a build-time dependency and fails",
861
+ " the build."
862
+ ].join("\n")
863
+ },
864
+ {
865
+ id: "v3/remove-workflow-exec-job-function",
866
+ name: "workflow.execJobFunction (imported) removed — use job.start()",
867
+ description: "`execJobFunction` on the `workflow` value imported from @tailor-platform/sdk/runtime(/workflow) is removed in v3. Calling it directly from a workflow job body to reach another job is not detected as a build-time dependency and has no working use; the target job's own `.start()` method is the only supported way to call it. This does not affect the ambient `tailor.workflow.execJobFunction` global — that's what `.start()` itself compiles down to at build time, and it remains fully supported.",
868
+ since: "1.0.0",
869
+ until: "3.0.0",
870
+ filePatterns: ["**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"],
871
+ suspiciousPatterns: ["execJobFunction"],
872
+ examples: [{
873
+ before: "import { workflow } from \"@tailor-platform/sdk/runtime\";\n\nawait workflow.execJobFunction(\"worker\", { id: 1 });",
874
+ after: "import { worker } from \"./jobs/worker\";\n\nawait worker.start({ id: 1 });"
875
+ }],
876
+ prompt: [
877
+ "workflow.execJobFunction — the value imported from",
878
+ "@tailor-platform/sdk/runtime or @tailor-platform/sdk/runtime/workflow — is",
879
+ "removed in v3. Replace each call with the target job's own .start() method:",
880
+ "import the WorkflowJob the call names and call <job>.start(args, options)",
881
+ "instead of workflow.execJobFunction(\"<job-name>\", args, options).",
882
+ "",
883
+ "This only removes the re-export from @tailor-platform/sdk/runtime. It does",
884
+ "not affect the ambient tailor.workflow.execJobFunction global, which stays",
885
+ "fully supported and is what .start() itself compiles down to at build time.",
886
+ "This codemod does not rewrite ambient tailor.workflow.execJobFunction(...)",
887
+ "call sites, since removing the import re-export does not affect them — but",
888
+ "if such a call site sits inside workflow job source and calls another job",
889
+ "by name, a separate build-time check already rejects it; migrate that call",
890
+ "to the target job's own .start() method too.",
891
+ "",
892
+ "If the job name passed to execJobFunction is not a string literal (a truly",
893
+ "dynamic dispatch), there is currently no supported replacement — .start()",
894
+ "only targets a statically known job. Flag this case for a human instead of",
895
+ "guessing a rewrite."
853
896
  ].join("\n")
854
897
  },
855
898
  {
@@ -1036,6 +1079,15 @@ const allCodemods = [
1036
1079
  prereleaseUntil: V2_NEXT_1,
1037
1080
  notice: true
1038
1081
  },
1082
+ {
1083
+ id: "v2/tailordb-timestamps-required",
1084
+ name: "`db.fields.timestamps()`: `updatedAt` becomes required",
1085
+ description: "The `updatedAt` field from `db.fields.timestamps()` changes from optional to required (non-null): it defaults to the current time and keeps refreshing automatically on every update, though a value you provide explicitly is still respected. Applying this change to a table that already has rows with `updatedAt: null` makes `deploy` fail with `field \"updatedAt\" cannot be updated from non-required to required when records with null values exist`. Backfill those rows first, e.g. `UPDATE <table> SET \"updatedAt\" = \"createdAt\" WHERE \"updatedAt\" IS NULL` for each affected table — see [TailorDB migrations](../services/tailordb-migration.md#performance-and-large-tables) for splitting a large backfill across primary-key ranges if a single `UPDATE` times out.",
1086
+ since: "1.0.0",
1087
+ until: "2.0.0",
1088
+ prereleaseUntil: V2_NEXT_2,
1089
+ notice: true
1090
+ },
1039
1091
  {
1040
1092
  id: "v2/rename-bin",
1041
1093
  name: "tailor-sdk binary → tailor",
@@ -1450,7 +1502,7 @@ const allCodemods = [
1450
1502
  {
1451
1503
  id: "v2/node-minimum-22-15-0",
1452
1504
  name: "Node.js minimum version raised to 22.15.0",
1453
- description: "v2 requires Node.js **22.15.0** or later. This is the first version that includes `module.registerHooks()`, which the SDK uses to register its TypeScript loader hook synchronously in the main thread. The actual floor is now **22.18.0**: Node 22.15.0–22.17.x has a bug ([nodejs/node#58607](https://github.com/nodejs/node/issues/58607)) that crashes `tailor seed validate` when requiring `node:`-scheme-only builtins such as `node:sqlite`, fixed upstream in 22.18.0. No source change is required; ensure your environment runs Node.js 22.18.0+.",
1505
+ description: "v2 requires Node.js **22.15.0** or later. This is the first version that includes `module.registerHooks()`, which the SDK uses to register its TypeScript loader hook synchronously in the main thread. The actual floor is now **22.18.0**: Node 22.15.0–22.17.x has a bug that crashes `tailor seed validate` when requiring `node:`-scheme-only builtins such as `node:sqlite`, fixed upstream by [nodejs/node#58612](https://github.com/nodejs/node/pull/58612) in 22.18.0. No source change is required; ensure your environment runs Node.js 22.18.0+.",
1454
1506
  since: "1.0.0",
1455
1507
  until: "2.0.0",
1456
1508
  notice: true
@@ -1463,6 +1515,14 @@ const allCodemods = [
1463
1515
  until: "2.0.0",
1464
1516
  notice: true
1465
1517
  },
1518
+ {
1519
+ id: "v2/tailordb-script-hash-migration-sync",
1520
+ name: "First v2 deploy to a v1-deployed environment needs migration sync",
1521
+ description: "The pre-v2 CLI never wrote a script hash into deployed schemas, so the first `tailor deploy` against an environment last deployed with it reports `Remote schema drift detected` with every scripted type showing `has no script hash on remote`. Run `tailor tailordb migration sync <current migration number>` once for that environment to write the missing hashes, then `tailor deploy` as usual. Preview/PR workspaces don't hit this, since they're built with v2 from the start. No source change is required.",
1522
+ since: "1.0.0",
1523
+ until: "2.0.0",
1524
+ notice: true
1525
+ },
1466
1526
  {
1467
1527
  id: "v2/dts-env-value-types",
1468
1528
  name: "tailor.d.ts Env uses value types instead of literal values",
@@ -1525,6 +1585,68 @@ const allCodemods = [
1525
1585
  "`function run`. Leave prose that merely mentions the old subcommand name",
1526
1586
  "unchanged unless it documents a command to type."
1527
1587
  ].join("\n")
1588
+ },
1589
+ {
1590
+ id: "v3/setup-branch-flag-rename",
1591
+ name: "setup branch --branch → --target",
1592
+ description: "Rename the `--branch` option of `tailor setup branch` invocations to `--target`. `--branch` remains as a deprecated alias until it is removed in v3. The `--branch` option of `setup tag`, `setup preview`, and `setup coordinate` is unchanged.",
1593
+ since: "1.72.0",
1594
+ until: "3.0.0",
1595
+ scriptPath: "v3/setup-branch-flag-rename/scripts/transform.js",
1596
+ filePatterns: [
1597
+ "**/package.json",
1598
+ "**/*.{sh,bash,zsh,ps1,cmd,bat,yml,yaml}",
1599
+ "**/*.md",
1600
+ "**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"
1601
+ ],
1602
+ legacyPatterns: [SETUP_BRANCH_RESIDUAL_FLAG],
1603
+ sourceStringLegacyPatterns: [SETUP_BRANCH_RESIDUAL_FLAG],
1604
+ examples: [{
1605
+ lang: "sh",
1606
+ before: "tailor setup branch --name my-app-stg --branch main",
1607
+ after: "tailor setup branch --name my-app-stg --target main"
1608
+ }],
1609
+ prompt: [
1610
+ "The `--branch` option of `tailor setup branch` is renamed to `--target`;",
1611
+ "the old spelling is removed in v3. Replace any remaining `--branch` options of",
1612
+ "`setup branch` invocations the codemod did not rewrite (e.g. wrapped across",
1613
+ "lines or invoked through a package runner such as `npx @tailor-platform/sdk`)",
1614
+ "with `--target`. Do not touch the `--branch` option of `setup tag`,",
1615
+ "`setup preview`, or `setup coordinate`, which keeps its name, and leave prose",
1616
+ "that merely mentions the option unchanged unless it documents a command to type."
1617
+ ].join("\n")
1618
+ },
1619
+ {
1620
+ id: "v3/relation-toward-table",
1621
+ name: "relation() toward.type → toward.table",
1622
+ description: "Rename the `.relation()` option `toward.type` to `toward.table`, matching the `db.type()` → `db.table()` rename. The relation's own `type` (its cardinality, e.g. `\"n-1\"`) is unchanged — only the target-table reference nested under `toward` moves.",
1623
+ since: "1.0.0",
1624
+ until: "3.0.0",
1625
+ scriptPath: "v3/relation-toward-table/scripts/transform.js",
1626
+ filePatterns: ["**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"],
1627
+ examples: [{
1628
+ before: [
1629
+ "customerId: db.uuid().relation({",
1630
+ " type: \"n-1\",",
1631
+ " toward: { type: customer },",
1632
+ "}),"
1633
+ ].join("\n"),
1634
+ after: [
1635
+ "customerId: db.uuid().relation({",
1636
+ " type: \"n-1\",",
1637
+ " toward: { table: customer },",
1638
+ "}),"
1639
+ ].join("\n")
1640
+ }],
1641
+ prompt: [
1642
+ "In Tailor SDK v3, `.relation()`'s `toward.type` option is renamed to",
1643
+ "`toward.table` (it names a target table, not a TypeScript/GraphQL type).",
1644
+ "Rename any remaining `toward.type` the codemod did not rewrite (e.g. a",
1645
+ "`toward` object reached through a shared variable, spread, or computed",
1646
+ "key) to `toward.table`. Do not touch the relation's own outer `type`",
1647
+ "property, which is the relation's cardinality (e.g. \"n-1\", \"1-1\",",
1648
+ "\"keyOnly\") and keeps its name."
1649
+ ].join("\n")
1528
1650
  }
1529
1651
  ];
1530
1652
  /**
@@ -2025,6 +2147,27 @@ function legacyPatternWarnings(relative, content, sourceStringContent, sourceTex
2025
2147
  return [`${relative}: contains ${Array.from(found).join(", ")} but was not migrated automatically (rule: ${lt.id}). Manual migration may be needed.`];
2026
2148
  });
2027
2149
  }
2150
+ function lineRemapper(before, after) {
2151
+ const hunks = structuredPatch("", "", before, after, "", "", { context: 0 }).hunks;
2152
+ const lastLine = Math.max(1, after.split("\n").length - (after.endsWith("\n") ? 1 : 0));
2153
+ const map = (line) => {
2154
+ let offset = 0;
2155
+ for (const hunk of hunks) {
2156
+ if (line < hunk.oldStart) break;
2157
+ if (hunk.oldLines === 0) {
2158
+ offset += hunk.newLines;
2159
+ continue;
2160
+ }
2161
+ if (line < hunk.oldStart + hunk.oldLines) {
2162
+ if (hunk.newLines === 0) return hunk.newStart;
2163
+ return Math.min(hunk.newStart + (line - hunk.oldStart), hunk.newStart + hunk.newLines - 1);
2164
+ }
2165
+ offset += hunk.newLines - hunk.oldLines;
2166
+ }
2167
+ return line + offset;
2168
+ };
2169
+ return (line) => Math.min(lastLine, Math.max(1, map(line)));
2170
+ }
2028
2171
  function compareReviewFindings(a, b) {
2029
2172
  return a.file.localeCompare(b.file) || a.line - b.line || a.message.localeCompare(b.message) || a.excerpt.localeCompare(b.excerpt);
2030
2173
  }
@@ -2032,6 +2175,9 @@ function compareReviewFindings(a, b) {
2032
2175
  * Run multiple codemods on a project directory using in-memory chaining.
2033
2176
  * Each file is processed through all transforms whose filePatterns match it.
2034
2177
  * Later transforms see earlier transforms' output — even in dry-run mode.
2178
+ * Review detection (reviewFindings and suspicious patterns) runs against each
2179
+ * codemod's own position in the transform chain, so a later codemod's rewrite
2180
+ * cannot hide an earlier codemod's findings.
2035
2181
  *
2036
2182
  * In dry-run mode, colorized diffs are printed to stderr.
2037
2183
  * @param codemods - Codemod packages to run (with resolved script paths)
@@ -2077,25 +2223,41 @@ async function runCodemods(codemods, targetPath, dryRun) {
2077
2223
  continue;
2078
2224
  }
2079
2225
  let current = original;
2226
+ const reviewTargets = [];
2080
2227
  for (const lt of matchedTransforms) {
2081
- if (!lt.transform) continue;
2082
- const result = await lt.transform(current, absolute);
2083
- if (result != null) {
2084
- current = result;
2085
- appliedCodemodIds.add(lt.id);
2228
+ if (lt.transform) {
2229
+ const result = await lt.transform(current, absolute);
2230
+ if (result != null) {
2231
+ current = result;
2232
+ appliedCodemodIds.add(lt.id);
2233
+ }
2086
2234
  }
2235
+ if (lt.prompt) reviewTargets.push({
2236
+ lt,
2237
+ snapshot: current
2238
+ });
2087
2239
  }
2088
2240
  if (current !== original) {
2089
2241
  filesModified.push(absolute);
2090
2242
  if (dryRun) printDiff(absolute, original, current);
2091
2243
  else await fs.promises.writeFile(absolute, current, "utf-8");
2092
2244
  }
2093
- const residualContent = contentForResidualMatching(relative, current);
2094
- const sourceStringContent = sourceStringContentForResidualMatching(relative, current);
2245
+ const residualBySnapshot = /* @__PURE__ */ new Map();
2246
+ const residualFor = (content) => {
2247
+ let entry = residualBySnapshot.get(content);
2248
+ if (!entry) {
2249
+ entry = {
2250
+ residual: contentForResidualMatching(relative, content),
2251
+ sourceString: sourceStringContentForResidualMatching(relative, content)
2252
+ };
2253
+ residualBySnapshot.set(content, entry);
2254
+ }
2255
+ return entry;
2256
+ };
2257
+ const { residual: residualContent, sourceString: sourceStringContent } = residualFor(current);
2095
2258
  const sourceTextContent = sourceTextContentForResidualMatching(relative, current);
2096
2259
  warnings.push(...legacyPatternWarnings(relative, residualContent, sourceStringContent, sourceTextContent, matchedTransforms));
2097
- for (const lt of matchedTransforms) {
2098
- if (!lt.prompt) continue;
2260
+ for (const { lt, snapshot } of reviewTargets) {
2099
2261
  const filesForReview = () => {
2100
2262
  let files = suspiciousByCodemod.get(lt.id);
2101
2263
  if (!files) {
@@ -2105,7 +2267,14 @@ async function runCodemods(codemods, targetPath, dryRun) {
2105
2267
  return files;
2106
2268
  };
2107
2269
  if (lt.reviewFindings) {
2108
- const findings = await lt.reviewFindings(current, absolute, relative);
2270
+ let findings = await lt.reviewFindings(snapshot, absolute, relative);
2271
+ if (snapshot !== current && findings.length > 0) {
2272
+ const remap = lineRemapper(snapshot, current);
2273
+ findings = findings.map((finding) => ({
2274
+ ...finding,
2275
+ line: remap(finding.line)
2276
+ }));
2277
+ }
2109
2278
  if (findings.length > 0) {
2110
2279
  const files = filesForReview();
2111
2280
  for (const finding of findings) files.add(finding.file);
@@ -2117,7 +2286,9 @@ async function runCodemods(codemods, targetPath, dryRun) {
2117
2286
  existing.push(...findings);
2118
2287
  }
2119
2288
  }
2120
- if (lt.suspiciousPatterns.some((p) => matchResidualPattern(residualContent, p) !== null) || sourceStringContent != null && lt.sourceStringSuspiciousPatterns.some((p) => matchResidualPattern(sourceStringContent, p) !== null)) filesForReview().add(relative);
2289
+ if (lt.suspiciousPatterns.length === 0 && lt.sourceStringSuspiciousPatterns.length === 0) continue;
2290
+ const { residual, sourceString } = residualFor(snapshot);
2291
+ if (lt.suspiciousPatterns.some((p) => matchResidualPattern(residual, p) !== null) || sourceString != null && lt.sourceStringSuspiciousPatterns.some((p) => matchResidualPattern(sourceString, p) !== null)) filesForReview().add(relative);
2121
2292
  }
2122
2293
  }
2123
2294
  const llmReviews = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tailor-platform/sdk-codemod",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Codemod runner for Tailor Platform SDK upgrades",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -21,7 +21,7 @@
21
21
  "pathe": "2.0.3",
22
22
  "picomatch": "4.0.5",
23
23
  "pkg-types": "2.3.1",
24
- "politty": "0.11.6",
24
+ "politty": "0.11.9",
25
25
  "semver": "7.8.5",
26
26
  "zod": "4.4.3"
27
27
  },
@@ -30,7 +30,8 @@
30
30
  "@types/picomatch": "4.0.3",
31
31
  "@types/semver": "7.8.0",
32
32
  "eslint-plugin-zod": "4.9.1",
33
- "oxlint": "1.76.0",
33
+ "oxlint": "1.78.0",
34
+ "oxlint-tsgolint": "7.0.2001",
34
35
  "tsdown": "0.22.14",
35
36
  "typescript": "6.0.3",
36
37
  "vitest": "4.1.10",
@@ -42,11 +43,11 @@
42
43
  },
43
44
  "scripts": {
44
45
  "build": "tsdown",
45
- "lint": "oxlint .",
46
- "lint:fix": "oxlint . --fix",
46
+ "lint": "oxlint --type-aware .",
47
+ "lint:fix": "oxlint --type-aware . --fix",
48
+ "knip": "knip",
47
49
  "typecheck": "tsc --noEmit",
48
50
  "test": "vitest",
49
- "prepublish": "pnpm run build",
50
51
  "publint": "publint --strict"
51
52
  },
52
53
  "bin": {