nativeapptemplate-agent 0.1.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/CLAUDE.md +137 -0
- package/LICENSE +21 -0
- package/README.md +192 -0
- package/dist/adb.d.ts +2 -0
- package/dist/adb.d.ts.map +1 -0
- package/dist/adb.js +31 -0
- package/dist/adb.js.map +1 -0
- package/dist/agents/contract-extract.d.ts +26 -0
- package/dist/agents/contract-extract.d.ts.map +1 -0
- package/dist/agents/contract-extract.js +205 -0
- package/dist/agents/contract-extract.js.map +1 -0
- package/dist/agents/judge.d.ts +21 -0
- package/dist/agents/judge.d.ts.map +1 -0
- package/dist/agents/judge.js +119 -0
- package/dist/agents/judge.js.map +1 -0
- package/dist/agents/planner.d.ts +3 -0
- package/dist/agents/planner.d.ts.map +1 -0
- package/dist/agents/planner.js +154 -0
- package/dist/agents/planner.js.map +1 -0
- package/dist/agents/reviewer.d.ts +9 -0
- package/dist/agents/reviewer.d.ts.map +1 -0
- package/dist/agents/reviewer.js +108 -0
- package/dist/agents/reviewer.js.map +1 -0
- package/dist/agents/types.d.ts +54 -0
- package/dist/agents/types.d.ts.map +1 -0
- package/dist/agents/types.js +2 -0
- package/dist/agents/types.js.map +1 -0
- package/dist/agents/workers/android.d.ts +3 -0
- package/dist/agents/workers/android.d.ts.map +1 -0
- package/dist/agents/workers/android.js +116 -0
- package/dist/agents/workers/android.js.map +1 -0
- package/dist/agents/workers/ios.d.ts +3 -0
- package/dist/agents/workers/ios.d.ts.map +1 -0
- package/dist/agents/workers/ios.js +110 -0
- package/dist/agents/workers/ios.js.map +1 -0
- package/dist/agents/workers/rails.d.ts +3 -0
- package/dist/agents/workers/rails.d.ts.map +1 -0
- package/dist/agents/workers/rails.js +145 -0
- package/dist/agents/workers/rails.js.map +1 -0
- package/dist/dispatch.d.ts +3 -0
- package/dist/dispatch.d.ts.map +1 -0
- package/dist/dispatch.js +38 -0
- package/dist/dispatch.js.map +1 -0
- package/dist/env.d.ts +3 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +48 -0
- package/dist/env.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/ruby.d.ts +2 -0
- package/dist/ruby.d.ts.map +1 -0
- package/dist/ruby.js +21 -0
- package/dist/ruby.js.map +1 -0
- package/dist/slug.d.ts +3 -0
- package/dist/slug.d.ts.map +1 -0
- package/dist/slug.js +11 -0
- package/dist/slug.js.map +1 -0
- package/dist/stub.d.ts +3 -0
- package/dist/stub.d.ts.map +1 -0
- package/dist/stub.js +6 -0
- package/dist/stub.js.map +1 -0
- package/dist/trace.d.ts +3 -0
- package/dist/trace.d.ts.map +1 -0
- package/dist/trace.js +13 -0
- package/dist/trace.js.map +1 -0
- package/dist/validation/capture.d.ts +15 -0
- package/dist/validation/capture.d.ts.map +1 -0
- package/dist/validation/capture.js +141 -0
- package/dist/validation/capture.js.map +1 -0
- package/dist/validation/discover.d.ts +11 -0
- package/dist/validation/discover.d.ts.map +1 -0
- package/dist/validation/discover.js +113 -0
- package/dist/validation/discover.js.map +1 -0
- package/dist/validation/index.d.ts +17 -0
- package/dist/validation/index.d.ts.map +1 -0
- package/dist/validation/index.js +9 -0
- package/dist/validation/index.js.map +1 -0
- package/dist/validation/launch.d.ts +21 -0
- package/dist/validation/launch.d.ts.map +1 -0
- package/dist/validation/launch.js +109 -0
- package/dist/validation/launch.js.map +1 -0
- package/dist/validation/layer1.d.ts +16 -0
- package/dist/validation/layer1.d.ts.map +1 -0
- package/dist/validation/layer1.js +98 -0
- package/dist/validation/layer1.js.map +1 -0
- package/dist/validation/layer2.d.ts +17 -0
- package/dist/validation/layer2.d.ts.map +1 -0
- package/dist/validation/layer2.js +168 -0
- package/dist/validation/layer2.js.map +1 -0
- package/dist/validation/layer3.d.ts +22 -0
- package/dist/validation/layer3.d.ts.map +1 -0
- package/dist/validation/layer3.js +96 -0
- package/dist/validation/layer3.js.map +1 -0
- package/dist/validation/stage1.d.ts +17 -0
- package/dist/validation/stage1.d.ts.map +1 -0
- package/dist/validation/stage1.js +71 -0
- package/dist/validation/stage1.js.map +1 -0
- package/dist/validation/visual-judge.d.ts +25 -0
- package/dist/validation/visual-judge.d.ts.map +1 -0
- package/dist/validation/visual-judge.js +89 -0
- package/dist/validation/visual-judge.js.map +1 -0
- package/package.json +69 -0
- package/scripts/ruby/.gitkeep +0 -0
- package/scripts/ruby/rename.rb +221 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
2
|
+
import { runLayer3 } from "./layer3.js";
|
|
3
|
+
import { captureScreenshot } from "./capture.js";
|
|
4
|
+
import { installAndLaunch } from "./launch.js";
|
|
5
|
+
const DEFAULT_RENDER_WAIT_MS = 3_000;
|
|
6
|
+
// End-to-end Stage 1 visual judge for one platform:
|
|
7
|
+
// 1. install + launch the built app on the booted sim/emulator
|
|
8
|
+
// 2. wait for render
|
|
9
|
+
// 3. capture the home screen as PNG
|
|
10
|
+
// 4. send PNG + rubric to the Opus 4.7 vision judge (median-of-3 per criterion)
|
|
11
|
+
//
|
|
12
|
+
// Fail-fast: any step's failure short-circuits and surfaces in the result.
|
|
13
|
+
// Caller is responsible for: building the artifact (Layer 2 build mode),
|
|
14
|
+
// resolving artifactPath / bundleId / packageName from the slug, and ensuring
|
|
15
|
+
// a sim/emulator is booted.
|
|
16
|
+
export async function runVisualJudge(input) {
|
|
17
|
+
const launch = await installAndLaunch(input.platform === "ios"
|
|
18
|
+
? {
|
|
19
|
+
platform: "ios",
|
|
20
|
+
appPath: input.artifactPath,
|
|
21
|
+
bundleId: requireField(input.bundleId, "bundleId is required for platform=ios"),
|
|
22
|
+
}
|
|
23
|
+
: {
|
|
24
|
+
platform: "android",
|
|
25
|
+
apkPath: input.artifactPath,
|
|
26
|
+
packageName: requireField(input.packageName, "packageName is required for platform=android"),
|
|
27
|
+
});
|
|
28
|
+
if (!launch.ok) {
|
|
29
|
+
return {
|
|
30
|
+
ok: false,
|
|
31
|
+
launch,
|
|
32
|
+
error: `install/launch failed: ${launch.error ?? "unknown"}`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
await sleep(input.renderWaitMs ?? DEFAULT_RENDER_WAIT_MS);
|
|
36
|
+
const capture = await captureScreenshot({
|
|
37
|
+
platform: input.platform,
|
|
38
|
+
outPath: input.screenshotPath,
|
|
39
|
+
});
|
|
40
|
+
if (!capture.ok) {
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
launch,
|
|
44
|
+
error: `screenshot capture failed: ${capture.error ?? "unknown"}`,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const layer3 = await runLayer3({
|
|
48
|
+
screenshotPath: capture.path,
|
|
49
|
+
rubric: input.rubric,
|
|
50
|
+
spec: input.spec,
|
|
51
|
+
...(input.samplesPerCriterion !== undefined ? { samplesPerCriterion: input.samplesPerCriterion } : {}),
|
|
52
|
+
...(input.model !== undefined ? { model: input.model } : {}),
|
|
53
|
+
});
|
|
54
|
+
return {
|
|
55
|
+
ok: layer3.pass,
|
|
56
|
+
launch,
|
|
57
|
+
screenshotPath: capture.path,
|
|
58
|
+
layer3,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
// Default Stage 1 rubric — two Yes/No criteria covering substrate-leak
|
|
62
|
+
// detection and basic render sanity. Phrased so pass=true is the desired
|
|
63
|
+
// state on every criterion.
|
|
64
|
+
//
|
|
65
|
+
// Per docs/SPEC.md, Stage 1 captures the post-launch screen and is scoped
|
|
66
|
+
// to "catch egregious rename failures" — substrate-leak detection plus
|
|
67
|
+
// 'does anything render at all'. Domain-semantic matching (e.g. "does this
|
|
68
|
+
// read as a clinic queue?") requires reaching the actual domain UI past
|
|
69
|
+
// onboarding/login and lives in Stage 2 (mobile-mcp navigation), where
|
|
70
|
+
// the agent can drive the app to a list/detail/form screen before
|
|
71
|
+
// judging. Adding a domain-match criterion here would false-fail any
|
|
72
|
+
// substrate that ships with onboarding-before-domain-UI — which is the
|
|
73
|
+
// substrate's intentional design.
|
|
74
|
+
export const DEFAULT_STAGE1_RUBRIC = [
|
|
75
|
+
{
|
|
76
|
+
id: "no-substrate-leak",
|
|
77
|
+
question: "Is the screen free of substrate-original tokens like 'Shop', 'Shopkeeper', 'ItemTag', 'NativeAppTemplate', or 'Number Tag'?",
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: "renders-cleanly",
|
|
81
|
+
question: "Does the screen render without an actual rendering failure — that is, no crash dialog, no broken-image-icon glyphs, no text overlapping other text, no content cut off the side of the screen? A welcome / launch / onboarding screen with decorative graphics counts as PASS as long as nothing is technically broken; do not judge whether the screen looks 'finished' or shows the app's domain content.",
|
|
82
|
+
},
|
|
83
|
+
];
|
|
84
|
+
function requireField(value, message) {
|
|
85
|
+
if (value === undefined)
|
|
86
|
+
throw new Error(message);
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=visual-judge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"visual-judge.js","sourceRoot":"","sources":["../../src/validation/visual-judge.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nativeapptemplate-agent",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A Claude Code agent that turns a natural-language SaaS spec into a working three-platform implementation (Rails API + SwiftUI iOS + Jetpack Compose Android).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"nativeapptemplate-agent": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist/",
|
|
12
|
+
"scripts/ruby/",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE",
|
|
15
|
+
"CLAUDE.md"
|
|
16
|
+
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=22"
|
|
19
|
+
},
|
|
20
|
+
"directories": {
|
|
21
|
+
"doc": "docs"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc",
|
|
25
|
+
"dev": "tsx src/index.ts",
|
|
26
|
+
"start": "node dist/index.js",
|
|
27
|
+
"typecheck:tests": "tsc --noEmit -p tsconfig.test.json",
|
|
28
|
+
"test": "NATIVEAPPTEMPLATE_STUB_ALL=1 node --import tsx --test tests/smoke.test.ts",
|
|
29
|
+
"ci": "npm run build && npm run typecheck:tests && npm test",
|
|
30
|
+
"cost": "bash scripts/cost.sh",
|
|
31
|
+
"prepublishOnly": "npm run ci"
|
|
32
|
+
},
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/nativeapptemplate/nativeapptemplate-agent.git"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/nativeapptemplate/nativeapptemplate-agent#readme",
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/nativeapptemplate/nativeapptemplate-agent/issues"
|
|
40
|
+
},
|
|
41
|
+
"keywords": [
|
|
42
|
+
"claude",
|
|
43
|
+
"agent",
|
|
44
|
+
"code-generation",
|
|
45
|
+
"saas",
|
|
46
|
+
"ios",
|
|
47
|
+
"android",
|
|
48
|
+
"rails",
|
|
49
|
+
"swift",
|
|
50
|
+
"kotlin",
|
|
51
|
+
"boilerplate",
|
|
52
|
+
"generator",
|
|
53
|
+
"anthropic",
|
|
54
|
+
"claude-agent-sdk",
|
|
55
|
+
"mcp"
|
|
56
|
+
],
|
|
57
|
+
"author": "Daisuke Adachi",
|
|
58
|
+
"license": "MIT",
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@types/node": "^25.6.0",
|
|
61
|
+
"tsx": "^4.21.0",
|
|
62
|
+
"typescript": "^6.0.3"
|
|
63
|
+
},
|
|
64
|
+
"dependencies": {
|
|
65
|
+
"@anthropic-ai/claude-agent-sdk": "^0.2.114",
|
|
66
|
+
"@anthropic-ai/sdk": "^0.90.0",
|
|
67
|
+
"yaml": "^2.8.4"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
#
|
|
3
|
+
# Apply a rename plan to a Rails project tree.
|
|
4
|
+
# Stdin: { "renamePlan": [{"from":"Shop","to":"Clinic"}, ...], "root": "/abs/path" }
|
|
5
|
+
# Stdout: { "files_scanned": N, "files_changed": N, "substitutions": N, "files_renamed": N }
|
|
6
|
+
#
|
|
7
|
+
# Rewrites file content AND renames files/directories for every
|
|
8
|
+
# PascalCase / snake_case / plural variant of each rename pair.
|
|
9
|
+
# Word-boundary matching via Regexp; crude-but-sufficient English
|
|
10
|
+
# pluralization. Skips .git, node_modules, tmp/, log/, vendor/bundle/.
|
|
11
|
+
|
|
12
|
+
require "json"
|
|
13
|
+
|
|
14
|
+
input = JSON.parse($stdin.read)
|
|
15
|
+
plan = input.fetch("renamePlan")
|
|
16
|
+
root = input.fetch("root")
|
|
17
|
+
|
|
18
|
+
stats = { files_scanned: 0, files_changed: 0, substitutions: 0, files_renamed: 0 }
|
|
19
|
+
|
|
20
|
+
SKIP_DIR_SEGMENTS = %w[.git node_modules tmp log DerivedData Pods Carthage xcuserdata .build build .gradle .idea .kotlin captures].freeze
|
|
21
|
+
SKIP_SUBPATHS = %w[vendor/bundle].freeze
|
|
22
|
+
TEXT_EXTS = %w[
|
|
23
|
+
.rb .erb .yml .yaml .json .md .gemspec .rake .ru .txt .sample .example .conf
|
|
24
|
+
.html .css .scss .js .mjs .tt .lock
|
|
25
|
+
.swift .plist .strings .xcconfig .entitlements .pbxproj .xcworkspacedata .modulemap .xcscheme
|
|
26
|
+
.kt .kts .xml .gradle .pro .toml .properties .cfg
|
|
27
|
+
.proto
|
|
28
|
+
].freeze
|
|
29
|
+
TEXT_BASENAMES = %w[
|
|
30
|
+
Gemfile Gemfile.lock Rakefile Procfile Procfile.dev
|
|
31
|
+
.gitignore .env.sample .ruby-version .node-version
|
|
32
|
+
config.ru Dockerfile
|
|
33
|
+
Podfile Podfile.lock Package.swift Cartfile Makefile
|
|
34
|
+
gradlew gradlew.bat gradle.properties local.properties
|
|
35
|
+
brakeman.ignore
|
|
36
|
+
].freeze
|
|
37
|
+
|
|
38
|
+
def pluralize(word)
|
|
39
|
+
if word.end_with?("y") && word.length > 1 && !%w[a e i o u].include?(word[-2])
|
|
40
|
+
word[0..-2] + "ies"
|
|
41
|
+
elsif word.end_with?("s", "sh", "ch", "x", "z")
|
|
42
|
+
word + "es"
|
|
43
|
+
else
|
|
44
|
+
word + "s"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def snake_case(pascal)
|
|
49
|
+
pascal.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
50
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
51
|
+
.downcase
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def camel_case(pascal)
|
|
55
|
+
return pascal if pascal.empty?
|
|
56
|
+
pascal[0].downcase + pascal[1..]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def humanize_lower(pascal)
|
|
60
|
+
snake_case(pascal).tr("_", " ")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def humanize_title(pascal)
|
|
64
|
+
humanize_lower(pascal).split(" ").map(&:capitalize).join(" ")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def first_char_up(s)
|
|
68
|
+
s.empty? ? s : s[0].upcase + s[1..]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def build_patterns(from, to)
|
|
72
|
+
from_snake = snake_case(from)
|
|
73
|
+
to_snake = snake_case(to)
|
|
74
|
+
from_snake_pl = pluralize(from_snake)
|
|
75
|
+
to_snake_pl = pluralize(to_snake)
|
|
76
|
+
from_pl = pluralize(from)
|
|
77
|
+
to_pl = pluralize(to)
|
|
78
|
+
from_flat = from.downcase
|
|
79
|
+
to_flat = to.downcase
|
|
80
|
+
from_flat_pl = from_flat + "s"
|
|
81
|
+
to_flat_pl = to_flat + "s"
|
|
82
|
+
from_camel = camel_case(from)
|
|
83
|
+
to_camel = camel_case(to)
|
|
84
|
+
from_camel_pl = pluralize(from_camel)
|
|
85
|
+
to_camel_pl = pluralize(to_camel)
|
|
86
|
+
from_human_lo = humanize_lower(from)
|
|
87
|
+
to_human_lo = humanize_lower(to)
|
|
88
|
+
from_human_ti = humanize_title(from)
|
|
89
|
+
to_human_ti = humanize_title(to)
|
|
90
|
+
from_human_se = first_char_up(from_human_lo)
|
|
91
|
+
to_human_se = first_char_up(to_human_lo)
|
|
92
|
+
from_human_lo_pl = from_snake_pl.tr("_", " ")
|
|
93
|
+
to_human_lo_pl = to_snake_pl.tr("_", " ")
|
|
94
|
+
from_human_ti_pl = from_human_lo_pl.split(" ").map(&:capitalize).join(" ")
|
|
95
|
+
to_human_ti_pl = to_human_lo_pl.split(" ").map(&:capitalize).join(" ")
|
|
96
|
+
from_human_se_pl = first_char_up(from_human_lo_pl)
|
|
97
|
+
to_human_se_pl = first_char_up(to_human_lo_pl)
|
|
98
|
+
|
|
99
|
+
# Ruby's \b treats `_` as a word char, so \bshop\b doesn't fire
|
|
100
|
+
# inside `shop_id` or `accounts_shopkeeper`. Hand-rolled boundaries:
|
|
101
|
+
# - PascalCase token: preceded by non-letter OR lowercase letter
|
|
102
|
+
# (PascalCase compound like `LoggedInShopkeeper`); followed by
|
|
103
|
+
# non-letter OR uppercase (next PascalCase word).
|
|
104
|
+
# - snake_case / lowercase token: preceded by non-letter;
|
|
105
|
+
# followed by non-letter OR uppercase (camelCase compound
|
|
106
|
+
# like `shopId`).
|
|
107
|
+
#
|
|
108
|
+
# Order matters: plural forms first so "Shops" isn't partially-
|
|
109
|
+
# matched as "Shop" + residual "s".
|
|
110
|
+
pascal_l = "(?:(?<![A-Za-z])|(?<=[a-z]))"
|
|
111
|
+
pascal_r = "(?:(?![A-Za-z])|(?=[A-Z]))"
|
|
112
|
+
snake_l = "(?<![A-Za-z])"
|
|
113
|
+
snake_r = "(?:(?![A-Za-z])|(?=[A-Z]))"
|
|
114
|
+
|
|
115
|
+
patterns = [
|
|
116
|
+
[/#{pascal_l}#{Regexp.escape(from_pl)}#{pascal_r}/, to_pl],
|
|
117
|
+
[/#{pascal_l}#{Regexp.escape(from)}#{pascal_r}/, to],
|
|
118
|
+
[/#{snake_l}#{Regexp.escape(from_snake_pl)}#{snake_r}/, to_snake_pl],
|
|
119
|
+
[/#{snake_l}#{Regexp.escape(from_snake)}#{snake_r}/, to_snake],
|
|
120
|
+
# flat-lowercase (runs AFTER snake so single-word rename pairs
|
|
121
|
+
# where flat == snake can't win over snake's replacement; catches
|
|
122
|
+
# multi-word compounds collapsed in URLs / package names /
|
|
123
|
+
# email addresses, e.g. `nativeapptemplate.com`).
|
|
124
|
+
[/#{snake_l}#{Regexp.escape(from_flat_pl)}#{snake_r}/, to_flat_pl],
|
|
125
|
+
[/#{snake_l}#{Regexp.escape(from_flat)}#{snake_r}/, to_flat],
|
|
126
|
+
# camelCase — first char lowercased, rest PascalCase. Catches
|
|
127
|
+
# the common Kotlin/Swift field-accessor form like `itemTag`
|
|
128
|
+
# (from `ItemTag`) or `itemTagInfoFromNdefMessage`.
|
|
129
|
+
[/#{snake_l}#{Regexp.escape(from_camel_pl)}#{snake_r}/, to_camel_pl],
|
|
130
|
+
[/#{snake_l}#{Regexp.escape(from_camel)}#{snake_r}/, to_camel],
|
|
131
|
+
[/#{snake_l}#{Regexp.escape(from_pl.upcase)}(?![A-Za-z])/, to_pl.upcase],
|
|
132
|
+
[/#{snake_l}#{Regexp.escape(from.upcase)}(?![A-Za-z])/, to.upcase],
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
# Humanized display forms — three case shapes per number:
|
|
136
|
+
# lower "item tag" / "item tags" (UI body text, prose)
|
|
137
|
+
# title "Item Tag" / "Item Tags" (headings, button labels)
|
|
138
|
+
# sentence "Item tag" / "Item tags" (sentence start, error messages,
|
|
139
|
+
# OpenAPI descriptions)
|
|
140
|
+
# Only meaningful when the PascalCase token is multi-word; for single
|
|
141
|
+
# words all three humanized forms collapse to the flat / Pascal forms
|
|
142
|
+
# already handled above. Plural patterns come first so "item tags"
|
|
143
|
+
# isn't partially matched as "item tag" + residual "s".
|
|
144
|
+
if from_human_lo.include?(" ")
|
|
145
|
+
boundary = "(?<![A-Za-z])"
|
|
146
|
+
boundary_r = "(?![A-Za-z])"
|
|
147
|
+
human_patterns = [
|
|
148
|
+
[/#{boundary}#{Regexp.escape(from_human_ti_pl)}#{boundary_r}/, to_human_ti_pl],
|
|
149
|
+
[/#{boundary}#{Regexp.escape(from_human_ti)}#{boundary_r}/, to_human_ti],
|
|
150
|
+
[/#{boundary}#{Regexp.escape(from_human_se_pl)}#{boundary_r}/, to_human_se_pl],
|
|
151
|
+
[/#{boundary}#{Regexp.escape(from_human_se)}#{boundary_r}/, to_human_se],
|
|
152
|
+
[/#{boundary}#{Regexp.escape(from_human_lo_pl)}#{boundary_r}/, to_human_lo_pl],
|
|
153
|
+
[/#{boundary}#{Regexp.escape(from_human_lo)}#{boundary_r}/, to_human_lo],
|
|
154
|
+
]
|
|
155
|
+
patterns = human_patterns + patterns
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
patterns
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
all_patterns = plan.flat_map { |p| build_patterns(p.fetch("from"), p.fetch("to")) }
|
|
162
|
+
|
|
163
|
+
def skip?(path)
|
|
164
|
+
return true if SKIP_DIR_SEGMENTS.any? { |d| path.include?("/#{d}/") || path.end_with?("/#{d}") }
|
|
165
|
+
return true if SKIP_SUBPATHS.any? { |sp| path.include?("/#{sp}") }
|
|
166
|
+
false
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def text_file?(path)
|
|
170
|
+
basename = File.basename(path)
|
|
171
|
+
return true if TEXT_BASENAMES.include?(basename)
|
|
172
|
+
return true if TEXT_EXTS.include?(File.extname(path))
|
|
173
|
+
false
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Pass 1 — rewrite file contents.
|
|
177
|
+
Dir.glob("#{root}/**/*", File::FNM_DOTMATCH).each do |path|
|
|
178
|
+
next unless File.file?(path)
|
|
179
|
+
next if skip?(path)
|
|
180
|
+
next unless text_file?(path)
|
|
181
|
+
|
|
182
|
+
stats[:files_scanned] += 1
|
|
183
|
+
|
|
184
|
+
begin
|
|
185
|
+
content = File.read(path, encoding: "UTF-8")
|
|
186
|
+
rescue StandardError
|
|
187
|
+
next
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
original = content.dup
|
|
191
|
+
local_subst = 0
|
|
192
|
+
all_patterns.each do |regex, replacement|
|
|
193
|
+
content = content.gsub(regex) { local_subst += 1; replacement }
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
next if content == original
|
|
197
|
+
|
|
198
|
+
File.write(path, content)
|
|
199
|
+
stats[:files_changed] += 1
|
|
200
|
+
stats[:substitutions] += local_subst
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Pass 2 — rename paths. Deepest-first so renaming a parent directory
|
|
204
|
+
# doesn't invalidate paths we haven't visited yet.
|
|
205
|
+
Dir.glob("#{root}/**/*", File::FNM_DOTMATCH).sort_by { |p| -p.length }.each do |path|
|
|
206
|
+
next if skip?(path)
|
|
207
|
+
next unless File.exist?(path)
|
|
208
|
+
|
|
209
|
+
old_name = File.basename(path)
|
|
210
|
+
new_name = old_name.dup
|
|
211
|
+
all_patterns.each { |regex, replacement| new_name = new_name.gsub(regex, replacement) }
|
|
212
|
+
next if new_name == old_name
|
|
213
|
+
|
|
214
|
+
new_path = File.join(File.dirname(path), new_name)
|
|
215
|
+
next if File.exist?(new_path)
|
|
216
|
+
|
|
217
|
+
File.rename(path, new_path)
|
|
218
|
+
stats[:files_renamed] += 1
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
puts JSON.generate(stats)
|