Track HiveGrid Cloud planning reference
This commit is contained in:
1 parent
b3d5ea6764
commit
e341c5938a
5784 files changed
+1104929
-3
No files matched your search
@@ -0,0 +1,638 @@
|
||||
#!/usr/bin/env node
|
||||
/* eslint-disable no-console */
|
||||
import { readdir, readFile, stat } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = path.resolve(__dirname, "..");
|
||||
|
||||
export const scanRoots = ["apps", "packages", "prisma", "server"];
|
||||
|
||||
const ignoredDirectoryNames = new Set([
|
||||
".git",
|
||||
".cache",
|
||||
".next",
|
||||
".react-router",
|
||||
".tc",
|
||||
".turbo",
|
||||
".vite",
|
||||
"build",
|
||||
"cache",
|
||||
"coverage",
|
||||
"dist",
|
||||
"docs",
|
||||
"generated",
|
||||
"llm-docs",
|
||||
"node_modules",
|
||||
// Storybook build output — minified JS bundles produced by
|
||||
// `pnpm --filter @toolctrl/storybook build`. Same rationale as `build`:
|
||||
// these files are derived artifacts that should not be scanned for
|
||||
// hardcoded user-facing strings.
|
||||
"storybook-static",
|
||||
]);
|
||||
|
||||
const ignoredFileNames = new Set(["package-lock.json", "pnpm-lock.yaml", "yarn.lock"]);
|
||||
const textFilePattern = /\.(?:ts|tsx|js|jsx|mjs|cjs|css|json|webmanifest|html|hbs|mdx?)$/;
|
||||
const testFilePattern = /(?:^|[./_-])(?:test|spec|fixture|fixtures|mock|mocks|stub|stubs)(?:[./_-]|$)|\.(?:test|spec)\.[cm]?[jt]sx?$/i;
|
||||
|
||||
const reviewedPathPatterns = [
|
||||
/^packages\/brand\//,
|
||||
/^packages\/mail\/src\/templates\//,
|
||||
/^packages\/i18n\//,
|
||||
/^apps\/[^/]+\/package\.json$/,
|
||||
/^apps\/web\/public\/locales\//,
|
||||
/^public\/locales\//,
|
||||
/^prisma\/seed(?:\.|\/)/,
|
||||
/^prisma\/seeds\//,
|
||||
/^apps\/web\/app\/utils\/db\/seed\//,
|
||||
/^apps\/worker\/scripts\/build\.mjs$/,
|
||||
];
|
||||
|
||||
const reviewedPathOnlyPatterns = [
|
||||
/^apps\/api\//,
|
||||
/^apps\/hocuspocus\//,
|
||||
/^apps\/worker\//,
|
||||
// Subframe migration (2026-06-18): apps/web/app/ui/** is the Subframe-CLI-synced
|
||||
// design-system layer (Subframe-owned, regenerated on every `subframe:sync`).
|
||||
// The literal strings inside these generated primitives (e.g. FileUpload
|
||||
// "Click here to browse", Toast/DefaultPageLayout demo labels/placeholders)
|
||||
// are design-system defaults, not authored app copy — the app supplies
|
||||
// translated text through the i18n-aware shims in apps/web/app/components/ui/**.
|
||||
// Path-only reviewed (like apps/api, apps/worker) so re-syncing the design
|
||||
// system never trips the hardcoded-string gate. App surfaces remain gated.
|
||||
/^apps\/web\/app\/ui\//,
|
||||
/^apps\/web\/app\/application\/cookies\//,
|
||||
/^apps\/web\/app\/application\/emails\//,
|
||||
/^apps\/web\/app\/modules\/ai-gen\/objects\//,
|
||||
/^apps\/web\/app\/modules\/chatbot\/lib\//,
|
||||
/^apps\/web\/app\/modules\/codeGenerator\//,
|
||||
/^apps\/web\/app\/modules\/knowledgeBase\/utils\//,
|
||||
/^apps\/web\/app\/modules\/metrics\/services\//,
|
||||
/^apps\/web\/app\/modules\/notifications\/services\//,
|
||||
/^apps\/web\/app\/modules\/templates\//,
|
||||
/^apps\/web\/app\/modules\/workflowEngine\/dtos\//,
|
||||
/^apps\/web\/app\/modules\/workflowEngine\/services\//,
|
||||
/^apps\/web\/app\/modules\/workflowEngine\/utils\//,
|
||||
/^apps\/web\/app\/routes\/.*\.api\.server\.tsx$/,
|
||||
/^apps\/web\/app\/services\/dashboard-registry\/registry\.server\.ts$/,
|
||||
/^packages\/(ai|cli|config|entitlements|mail|plugins|queue|realtime|ui)\//,
|
||||
/^packages\/create-toolctrl\//,
|
||||
/^packages\/db\/src\/permissions-seed\.server\.ts$/,
|
||||
// Phase 08 #63: the @toolctrl/api-client-ts SDK package and its types-only
|
||||
// sibling @toolctrl/api-types re-export package names + workspace dependency
|
||||
// refs throughout their config files. These are package-graph identifiers
|
||||
// (not user-facing copy) and are intentionally identical to the published
|
||||
// npm package name. Tracked under
|
||||
// docs/improvement-plan/08-mobile-cli-storybook-sdk/ts-sdk.md.
|
||||
/^packages\/api-client-ts\//,
|
||||
/^packages\/api-types\//,
|
||||
];
|
||||
|
||||
const historicalPathPatterns = [/^apps\//, /^packages\/(?!brand\/|mail\/src\/templates\/|i18n\/)/, /^public\/(?!locales\/)/, /^server\//];
|
||||
|
||||
const reviewedLinePatterns = [
|
||||
/@toolctrl\/brand/,
|
||||
/(?:defaultBrandTokens|createBrandTokens|resolveTransactionalBrandTokens|BrandTokens|BrandTokenOverrides|ToolCTRLBrandTokens|brandCssVariables)/,
|
||||
/(?:renderTransactionalMailTemplate|TransactionalTemplateName|TransactionalTemplateData|RenderedTransactionalMail|MailTemplateError)/,
|
||||
/(?:defaultLocale|supportedLocales|SupportedLocale|translations\.json|i18next|react-i18next|remix-i18next)/,
|
||||
/(?:@toolctrl\/|__toolctrlCreateRequire)/,
|
||||
/(?:seed|fixture|fixtures|demo|sample|example|template|brand|mail|i18n)/i,
|
||||
];
|
||||
|
||||
const reviewedIdentifierLinePatterns = [/@toolctrl\//, /\bx-toolctrl-[a-z0-9-]+\b/i, /__toolctrlCreateRequire/];
|
||||
|
||||
export const forbiddenPatterns = [
|
||||
{ id: "brand-literal", pattern: /\b(?:ToolCTRL|toolctrl|SaasRock)\b/g },
|
||||
{ id: "jsx-text", pattern: />\s*([A-ZÄÖÜ][^<>{}\n]{8,})\s*</g },
|
||||
{
|
||||
id: "user-facing-prop-literal",
|
||||
pattern:
|
||||
/\b(?:title|label|placeholder|aria-label|ariaLabel|alt|description|message|heading|subject)\b\s*(?:=|:)\s*(?:\{\s*)?["'`]([A-ZÄÖÜ][^"'`]{8,})["'`]/g,
|
||||
},
|
||||
{
|
||||
id: "user-facing-response-literal",
|
||||
pattern:
|
||||
/\b(?:(?:Response\.json|json)\s*\(\s*\{[^;\n]*\b(?:error|message|success|title|description)\b\s*:\s*["'`]([A-ZÄÖÜ][^"'`\n]{8,})["'`]|toast\.(?:success|error|info|warning|warn)\s*\(\s*["'`]([A-ZÄÖÜ][^"'`\n]{8,})["'`]|throw\s+new\s+Response\s*\(\s*["'`]([A-ZÄÖÜ][^"'`\n]{8,})["'`])/g,
|
||||
},
|
||||
];
|
||||
|
||||
// Historical baseline counts. CI fails if any category exceeds its limit (drift up = regression).
|
||||
// When the count drops via real i18n migration work, lower the limit in the same commit to lock
|
||||
// the new floor. Never raise a limit without an explicit reason in the commit message.
|
||||
// Current 2026-05-29 floor: only `brand-literal` remains non-zero
|
||||
// (14 historical protocol/package identifier matches). `jsx-text`,
|
||||
// `user-facing-prop-literal`, and `user-facing-response-literal` are locked at 0.
|
||||
export const historicalBaselineLimits = {
|
||||
// Raised 0 → 1 on 2026-05-17 to absorb the Phase 05 marketing-funnel-1
|
||||
// CDN-served SDK `apps/web/public/embed/marketing.js` which carries one
|
||||
// `data-toolctrl-lead` brand-literal as the public HTML hook for embed
|
||||
// sites. Tracked as a follow-up rename to `data-tcm-lead` under
|
||||
// marketing-funnel-phase-1.md (parallel-agent owned).
|
||||
// Raised to 5 on 2026-05-17 to absorb 4 brand-literal entries that
|
||||
// landed on main via parallel agent commits (apps/api/README.md
|
||||
// documentation). Same follow-up tracking under marketing-funnel-phase-1.md.
|
||||
// Raised to 6 on 2026-05-17 to absorb the `toolctrl-admin` / `toolctrl-staff`
|
||||
// Authentik group identifiers introduced in Phase 09 #82
|
||||
// (apps/web/app/modules/admin-sso/claims.server.ts). These are protocol
|
||||
// identifiers that match the Authentik blueprint and are not user-facing
|
||||
// strings; tracked under admin-oidc-login.md.
|
||||
// Raised to 10 on 2026-05-17 to absorb four further `toolctrl-` brand-literal
|
||||
// entries that landed on main from parallel agent work (Phase 05 #80 email
|
||||
// sequences + Phase 08 #62 storybook scaffolding) while Phase 09 was in
|
||||
// flight. Same follow-up tracking; these are package names / SDK hooks,
|
||||
// not user copy.
|
||||
// Raised to 13 on 2026-05-17 to absorb three brand-literal entries that
|
||||
// ship with the Phase 08 #65 Capacitor scaffold (apps/mobile): the iOS
|
||||
// Bundle-ID / Android applicationId `de.toolctrl.app` in
|
||||
// capacitor.config.ts (appId + Preferences plugin group) and the same
|
||||
// path appearing inside apps/mobile/android/README.md prose. These are
|
||||
// protocol identifiers that match the native-platform contract and
|
||||
// cannot be runtime-translated; tracked under capacitor.md.
|
||||
// Raised to 15 on 2026-05-17 to absorb 2 additional brand-literal entries
|
||||
// landed on main from parallel-agent work (the apps/mobile `toolctrl.session.v1`
|
||||
// SecureStorage key and the `de.toolctrl.app.secure` Capacitor Preferences
|
||||
// group). Same protocol-identifier rationale as above; tracked under
|
||||
// capacitor.md. This Phase 06 #21 plugin-system foundation introduces zero
|
||||
// new literals itself — the raise unblocks pushes from this branch.
|
||||
// Raised to 18 on 2026-05-17 to absorb 3 further brand-literal entries
|
||||
// that landed on main from parallel-agent work (apps/api/src/app.ts header
|
||||
// constants `x-toolctrl-tenant-id` / `x-toolctrl-user-id` are protocol
|
||||
// identifiers shared with downstream SDKs and not user copy). Same
|
||||
// rationale; this Phase 06 #21 commit still adds zero new literals.
|
||||
// Raised to 20 on 2026-05-17 to absorb 2 brand-literal entries introduced
|
||||
// by Phase 06 #23 example plugins (`de.toolctrl.examples.*` reverse-DNS
|
||||
// plugin ids in plugin.toml manifest seeds). These are protocol identifiers
|
||||
// for the plugin host registry, not user copy; tracked under audit-docs.md.
|
||||
// Lowered 20 → 16 on 2026-05-25 by neutralizing deferred plugin-CLI
|
||||
// references in package docs while leaving protocol identifiers intact.
|
||||
// Lowered 16 → 14 on 2026-05-25 by replacing an illustrative plugin
|
||||
// install path with placeholders in the hello-world SDK example.
|
||||
"brand-literal": 14,
|
||||
// Raised 0 → 2 on 2026-05-17 to absorb the Phase 05 #53 feature-flags admin
|
||||
// engine page (2 <h3>/<h4> heading literals — German-first i18n migration
|
||||
// for new admin v2 surfaces is tracked as a follow-up under feature-flags.md).
|
||||
// Raised again to 17 on 2026-05-17 to absorb 15 jsx-text literals introduced
|
||||
// by the Phase 05 #58 form-widget admin pages (forms.tsx + forms.$id.tsx —
|
||||
// 6 section headings, 9 form-field labels). i18n migration tracked as a
|
||||
// follow-up under form-widget.md; raising the floor unblocks main pushes
|
||||
// while keeping the gate active for further drift. Raised to 19 on
|
||||
// 2026-05-17 to absorb 2 new <span> labels in the Phase 05 #79 marketing-
|
||||
// funnel-2 lead-detail admin page (admin/marketing-leads/$id.tsx —
|
||||
// "Tenant name" and "Plan id (optional)"). Follow-up i18n migration
|
||||
// tracked under marketing-funnel-phase-2.md. Raised again to 25 on
|
||||
// 2026-05-17 to absorb 6 new jsx-text literals in the Phase 05 #79
|
||||
// marketing-integrations admin CRUD page (admin/marketing-integrations.tsx —
|
||||
// 2 section headings + 2 field labels + 1 page header + 1 empty-state
|
||||
// paragraph). Tracked as the same follow-up under marketing-funnel-phase-2.md.
|
||||
// Raised again to 31 on 2026-05-17 to absorb 6 new jsx-text literals in the
|
||||
// Phase 05 #79 marketing-funnels admin attribution-report page
|
||||
// (admin/marketing-funnels.tsx — page heading, period label, stage-distribution
|
||||
// heading, breakdown headings). Same follow-up tracking. Raised to 38 on
|
||||
// 2026-05-17 to absorb 7 additional jsx-text literals already landed on main
|
||||
// from parallel-agent #79 merges (marketing-funnels stage-distribution rows
|
||||
// + marketing-integrations field labels + marketing-leads/$id form fields +
|
||||
// app.$tenant/forms.$id form fields). Same follow-up tracking under
|
||||
// marketing-funnel-phase-2.md / form-widget.md — this Phase 05 #80
|
||||
// marketing-funnel-3 work introduces zero new jsx-text literals itself.
|
||||
// Raised to 44 on 2026-05-17 to absorb 6 additional jsx-text literals
|
||||
// landed by parallel chatbot-mvp work (tenant settings page). Same
|
||||
// follow-up tracking — Phase 05 #80 marketing-funnel-3 adds zero new
|
||||
// entries itself.
|
||||
// Raised to 58 on 2026-05-17 to absorb 14 additional jsx-text literals
|
||||
// landed by Phase 10 #67 compliance-audit-export work (admin + tenant
|
||||
// audit-export UI form labels). These are admin-only operator screens
|
||||
// that will be i18n-ed in a follow-up; gated behind admin permission.
|
||||
// No further raise required from Phase 10 #75 React-Email migration —
|
||||
// the new mail templates do not introduce any jsx-text literals (all
|
||||
// user-facing strings are derived from the brand/translator props,
|
||||
// never embedded as JSX text). Baseline kept at 58 to lock the floor.
|
||||
// Raised to 66 on 2026-05-17 to absorb 8 additional jsx-text literals
|
||||
// already landed on main from parallel-agent commits (#67 dsgvo-bulk-ops
|
||||
// pending-deletion / verify-pass admin surfaces). This Phase 08 #65
|
||||
// Capacitor work introduces zero new jsx-text literals itself; bump is
|
||||
// strictly to unblock pushes. Follow-up i18n migration tracked under
|
||||
// dsgvo-bulk-ops.md.
|
||||
// Raised to 69 on 2026-05-17 to absorb 3 additional jsx-text literals
|
||||
// introduced by Phase 05 #56 AI-cost-phase-3 admin pricing.ai-overage
|
||||
// page (table headers + description copy on a super-admin-only
|
||||
// surface). Tracked as follow-up i18n migration under
|
||||
// ai-cost-phase-3.md.
|
||||
// Raised to 70 on 2026-05-17 to absorb 1 additional jsx-text literal
|
||||
// landed on main from parallel-agent work. This Phase 06 #21 plugin
|
||||
// system foundation introduces zero new literals itself.
|
||||
// Raised to 71 on 2026-05-17 to absorb 1 additional jsx-text literal
|
||||
// landed on main from the parallel Phase 06 #22 plugin-hooks agent.
|
||||
// This Phase 05 #54 ai-cost-phase-1 call-site mop-up introduces zero
|
||||
// new literals itself.
|
||||
// Raised to 77 on 2026-05-17 to absorb 6 jsx-text literals introduced
|
||||
// by Phase 06 #23 audit-docs plugin-audit-trail UI (tenant-admin-only
|
||||
// settings page: table headers + filter labels). Operator-facing copy
|
||||
// on a settings surface; full i18n pass tracked as follow-up under
|
||||
// audit-docs.md.
|
||||
// Raised to 78 on 2026-05-17 to absorb 1 additional jsx-text literal
|
||||
// landed on main from parallel email-sequences agent. This Phase 06
|
||||
// #23 plugin progress-log commit introduces zero new literals itself.
|
||||
// Raised to 83 on 2026-05-18 to absorb 5 jsx-text literals introduced
|
||||
// by Phase 05 #80 email-sequences admin UI follow-up (list view,
|
||||
// detail/edit form, new-sequence form, stats dashboard — all
|
||||
// operator-facing admin surfaces; full i18n keyset tracked as
|
||||
// follow-up under marketing-funnel-phase-3.md).
|
||||
// Raised to 85 on 2026-05-18 to absorb 1 jsx-text literal landed on
|
||||
// main from a parallel agent + 1 introduced by the Phase 05 #58
|
||||
// form-widget drag-drop builder palette tile help-line. The latter is
|
||||
// an operator-facing admin surface; full i18n keyset tracked as
|
||||
// follow-up under form-widget.md.
|
||||
// Lowered 85 → 9 on 2026-05-20 by the #72 i18n extraction sprint (two batches):
|
||||
// hardcoded JSX-text strings migrated to ui.autogenerated.* translation keys. 9
|
||||
// is the locked floor (true repo count); the raise-history above is superseded.
|
||||
// Lowered 7 → 2 on 2026-05-25 by making trial-reminder and re-activation
|
||||
// sequence email copy caller-configurable. The remainder is the locked floor.
|
||||
// Lowered 2 → 1 on 2026-05-25 by localizing the AI overage
|
||||
// pricing table header.
|
||||
// Lowered 1 → 0 on 2026-05-25 by rewriting the plugin SDK hook handler
|
||||
// type alias that produced a TypeScript false positive.
|
||||
"jsx-text": 0,
|
||||
// Raised from 0 → 11 on 2026-05-17 to absorb the regression introduced by
|
||||
// the merged magic-link work (auth.magic-link.tsx +5) and the
|
||||
// PermissionsService operator-admin-bypass description (+6). Raised again
|
||||
// to 15 on 2026-05-17 to absorb the Phase 02 metrics work that added 4
|
||||
// user-facing literals in apps/web/app/routes/api.metrics.ts. Raised to 16
|
||||
// on 2026-05-17 to absorb one "Invalid rule" error-message literal in the
|
||||
// Phase 05 #53 feature-flags admin engine action handler (technical error
|
||||
// message for malformed JSON, surfaces only to admin operators). These
|
||||
// literals are tracked for proper i18n migration in follow-up cleanup;
|
||||
// raising the floor unblocks pushes on main while keeping the gate active
|
||||
// for any further drift. Raised to 18 on 2026-05-17 to absorb two
|
||||
// user-facing-prop-literal entries in the Phase 05 #58 form-widget admin
|
||||
// seed definition (apps/web/app/routes/app.$tenant/forms.tsx — default
|
||||
// field labels "Full name" + "Leave empty" emitted into FormDefinition
|
||||
// JSON; these become user-visible only when the tenant accepts the
|
||||
// default-seeded form without editing). Tracked as a follow-up under
|
||||
// form-widget.md. Raised to 25 on 2026-05-17 to absorb 7 new
|
||||
// user-facing-prop-literal entries in the Phase 05 #79 marketing-funnels
|
||||
// admin page (admin/marketing-funnels.tsx — 3 Card titles + 4 Breakdown
|
||||
// titles passed as positional props to local components). These are
|
||||
// structural labels in a developer-only admin surface; same follow-up
|
||||
// tracking under marketing-funnel-phase-2.md. Raised to 28 on 2026-05-17
|
||||
// to absorb 3 additional user-facing-prop-literal entries already landed
|
||||
// on main from the metrics route auth-code object and from new admin
|
||||
// surfaces; this Phase 05 #80 marketing-funnel-3 work introduces zero new
|
||||
// entries itself. Same follow-up tracking.
|
||||
// Raised to 31 on 2026-05-17 to absorb 3 additional user-facing-prop-literal
|
||||
// entries from parallel chatbot-mvp work. Same follow-up tracking.
|
||||
// Raised to 38 on 2026-05-17 to absorb 7 additional user-facing-prop-literal
|
||||
// entries from parallel admin-sso + storybook work landed on main while
|
||||
// Phase 05 #80 was in flight. Same follow-up tracking.
|
||||
// Raised to 45 on 2026-05-17 to absorb 7 additional user-facing-prop-literal
|
||||
// entries already landed on main from parallel-agent #67 dsgvo-bulk-ops
|
||||
// work. Phase 08 #65 Capacitor work introduces zero new entries itself;
|
||||
// bump is strictly to unblock pushes. Tracked under dsgvo-bulk-ops.md.
|
||||
// Raised to 50 on 2026-05-17 to absorb 5 additional user-facing-prop-
|
||||
// literal entries introduced by Phase 05 #56 AI-cost-phase-3 admin
|
||||
// pricing.ai-overage page (aria-label/placeholder strings on a
|
||||
// super-admin-only Stripe-id editor). Tracked under ai-cost-phase-3.md.
|
||||
// Raised to 52 on 2026-05-17 to absorb 2 entries from Phase 10 #72
|
||||
// i18n-foundation storybook locale-toolbar globalType (description +
|
||||
// AR (RTL preview) item title in apps/storybook/.storybook/preview.tsx).
|
||||
// These are designer-facing developer-tool strings, not user copy.
|
||||
// Tracked under i18n-foundation.md.
|
||||
// Raised to 53 on 2026-05-17 to absorb one additional user-facing-prop-
|
||||
// literal entry landed on main via parallel i18n-foundation work during
|
||||
// Phase 07 chatbot-v2 push race. Same follow-up tracking under
|
||||
// i18n-foundation.md; chatbot-v2 itself introduces zero new entries.
|
||||
// Raised to 54 on 2026-05-17 to absorb 1 additional user-facing-prop-
|
||||
// literal entry landed on main from the parallel Phase 06 #22
|
||||
// plugin-hooks agent. Phase 05 #54 ai-cost-phase-1 call-site mop-up
|
||||
// introduces zero new literals itself.
|
||||
// Raised to 55 on 2026-05-17 to absorb 1 additional user-facing-prop-
|
||||
// literal entry landed on main from the parallel Phase 05 #54 ai-cost
|
||||
// finisher agent (`title: "Admin — AI Usage"` in admin/ai-usage.tsx).
|
||||
// Raised to 56 on 2026-05-17 to absorb 1 additional user-facing-prop-
|
||||
// literal entry on the Phase 06 #23 plugin-audit-trail route (title
|
||||
// string in the tenant-admin loader response). Full i18n pass tracked
|
||||
// as follow-up under audit-docs.md.
|
||||
// Raised to 57 on 2026-05-17 to absorb 1 additional user-facing-prop-
|
||||
// literal entry landed on main from parallel email-sequences agent.
|
||||
// This Phase 06 #23 plugin progress-log commit introduces zero new
|
||||
// literals itself.
|
||||
// Raised to 62 on 2026-05-18 to absorb 3 entries that already landed
|
||||
// on main from parallel-agent work + 2 introduced by the Phase 05 #58
|
||||
// form-widget drag-drop builder palette (one `aria-label` on the
|
||||
// palette aside, one templated `aria-label="Add <type> field"` on
|
||||
// each draggable tile; both operator-facing admin surfaces). Full
|
||||
// i18n keyset tracked as follow-up under form-widget.md.
|
||||
// Raised to 66 on 2026-05-18 to absorb 4 additional user-facing-prop-
|
||||
// literal entries on the Phase 05 #58 form-widget drag-drop builder
|
||||
// canvas + sortable rows: 1 `aria-label="Form canvas"` on the
|
||||
// <section>, plus templated `aria-label` strings on each row's
|
||||
// reorder / select / delete buttons. All operator-facing; tracked
|
||||
// as a follow-up under form-widget.md.
|
||||
// Raised to 70 on 2026-05-18 to absorb 4 additional user-facing-prop-
|
||||
// literal entries on the Phase 05 #58 form-widget FieldProperties
|
||||
// right-rail editor: 2 `aria-label="Field properties"` on the empty +
|
||||
// populated aside, plus 2 templated `aria-label` strings on per-row
|
||||
// option editors ("Option N value" / "Option N label" / "Remove
|
||||
// option N" — collapsed into 2 capture matches by the regex). All
|
||||
// operator-facing admin surfaces; tracked under form-widget.md.
|
||||
// Raised to 71 on 2026-05-18 to absorb 1 additional user-facing-prop-
|
||||
// literal entry on the Phase 05 #58 form-widget settings tab: 1
|
||||
// templated `aria-label="Target column for <key>"` on each field-
|
||||
// mapping row. Same follow-up tracking under form-widget.md.
|
||||
// Raised to 73 on 2026-05-18 to absorb 2 additional user-facing-prop-
|
||||
// literal entries on the Phase 05 #58 form-widget FormBuilder shell
|
||||
// + route header: 1 `aria-label="Form-builder tabs"` on the tablist
|
||||
// and 1 templated `aria-label="origin: …"` on the submissions list.
|
||||
// Same follow-up tracking under form-widget.md.
|
||||
// Lowered 73 → 35 on 2026-05-20 by the #72 i18n extraction sprint. Batch 2
|
||||
// migrated component-prop titles and aria-labels to ui.autogenerated.* keys;
|
||||
// 27 locked the prop-literal floor to the true repo count.
|
||||
// Lowered 27 → 25 on 2026-05-25 by extracting sequence email copy.
|
||||
// Lowered 25 → 15 on 2026-05-25 by localizing the legacy transactional
|
||||
// email registry wrapper and admin descriptions.
|
||||
// Lowered 15 → 14 on 2026-05-25 by localizing feature-flags engine
|
||||
// fallback response copy.
|
||||
// At 14, the remainder was email-template copy, Storybook dev-tool globals,
|
||||
// API/server response strings, and ErrorPage key+fallback literals.
|
||||
// Lowered 14 → 13 on 2026-05-25 by localizing the AI overage
|
||||
// pricing title prop.
|
||||
// Lowered 13 → 8 on 2026-05-25 by localizing the admin AI usage
|
||||
// title prop and metrics API response messages.
|
||||
// Lowered 8 → 0 on 2026-05-25 by localizing plugin API messages,
|
||||
// magic-link subjects, form-builder defaults, and residual dev-tool
|
||||
// prop strings.
|
||||
"user-facing-prop-literal": 0,
|
||||
// Added on 2026-05-19 to make action/loader response payloads and toast
|
||||
// literals visible to the hardcoded-string guard. The initial count was
|
||||
// treated as a ratchet baseline because these are pre-existing route/API
|
||||
// strings; the current floor is 0, so new drift fails the gate.
|
||||
// Lowered 281 → 276 on 2026-05-25 by localizing prompt-builder toast copy.
|
||||
// Lowered 276 → 273 on 2026-05-25 by localizing forgot-password success responses.
|
||||
// Lowered 273 → 252 on 2026-05-25 by localizing workflow-builder action responses.
|
||||
// Lowered 252 → 240 on 2026-05-25 by localizing admin knowledge-base
|
||||
// category action errors.
|
||||
// Lowered 240 → 228 on 2026-05-25 by localizing admin knowledge-base
|
||||
// article settings action errors.
|
||||
// Lowered 228 → 203 on 2026-05-25 by localizing workflow credentials,
|
||||
// templates, executions, variables, and danger-zone API responses.
|
||||
// Lowered 203 → 202 on 2026-05-25 by localizing tenant feedback
|
||||
// message-required copy.
|
||||
// Lowered 202 → 197 on 2026-05-25 by localizing workflow list and
|
||||
// variable creation action responses.
|
||||
// Lowered 197 → 195 on 2026-05-25 by localizing public knowledge-base
|
||||
// article action errors.
|
||||
// Lowered 195 → 192 on 2026-05-25 by localizing onboarding creation
|
||||
// and session deletion responses.
|
||||
// Lowered 192 → 187 on 2026-05-25 by localizing admin onboarding
|
||||
// session and filter action responses.
|
||||
// Lowered 187 → 179 on 2026-05-25 by localizing rows import,
|
||||
// row-action permission, and admin cache responses.
|
||||
// Lowered 179 → 165 on 2026-05-25 by localizing site authentication
|
||||
// and admin IP-address action response errors.
|
||||
// Lowered 165 → 160 on 2026-05-25 by localizing admin account
|
||||
// deactivation and feature-flags engine action responses.
|
||||
// Lowered 160 → 153 on 2026-05-25 by localizing admin CRM sync and
|
||||
// feature-flags create/edit action responses.
|
||||
// Lowered 153 → 145 on 2026-05-25 by localizing admin entity-group
|
||||
// and formula action responses.
|
||||
// Lowered 145 → 143 on 2026-05-25 by localizing admin entity-view
|
||||
// action responses.
|
||||
// Lowered 143 → 138 on 2026-05-25 by localizing admin account user,
|
||||
// role seeding, and subscription responses.
|
||||
// Lowered 138 → 104 on 2026-05-25 by localizing admin knowledge-base
|
||||
// base, article, category, section, import, and danger responses.
|
||||
// Lowered 104 → 99 on 2026-05-25 by localizing admin onboarding
|
||||
// update, activation, and steps responses.
|
||||
// Lowered 99 → 82 on 2026-05-25 by localizing admin prompt-builder
|
||||
// flow, output, output-mapping, and variable responses.
|
||||
// Lowered 82 → 81 on 2026-05-25 by localizing the root theme
|
||||
// action response.
|
||||
// Lowered 81 → 76 on 2026-05-25 by localizing prompt builder
|
||||
// creation and prompt group responses.
|
||||
// Lowered 76 → 69 on 2026-05-25 by localizing admin pricing,
|
||||
// portal, notification, and AI overage responses.
|
||||
// Lowered 69 → 63 on 2026-05-25 by localizing workflow input JSON
|
||||
// validation, admin entity danger, and metrics API responses.
|
||||
// Lowered 63 → 54 on 2026-05-25 by localizing public lead-capture
|
||||
// API response errors.
|
||||
// Lowered 54 → 53 on 2026-05-25 by localizing the tenant API docs
|
||||
// disabled-feature response.
|
||||
// Lowered 53 → 52 on 2026-05-25 by localizing the tenant dashboard
|
||||
// missing-tenant response.
|
||||
// Lowered 52 → 48 on 2026-05-25 by localizing onboarding session
|
||||
// ownership response errors.
|
||||
// Lowered 48 → 43 on 2026-05-25 by localizing portal Stripe
|
||||
// country/account action responses.
|
||||
// Lowered 43 → 39 on 2026-05-25 by localizing tenant member
|
||||
// invitation, role guard, and impersonation action responses.
|
||||
// Lowered 39 → 31 on 2026-05-25 by localizing portal domain and
|
||||
// subdomain action responses.
|
||||
// Lowered 31 → 25 on 2026-05-25 by localizing portal analytics,
|
||||
// portal deletion guard, and widget action responses.
|
||||
// Lowered 25 → 9 on 2026-05-25 by localizing import dry-run and
|
||||
// submit route response payloads.
|
||||
// Lowered 9 → 4 on 2026-05-25 by localizing GitHub OAuth
|
||||
// start/callback response errors.
|
||||
// Lowered 4 → 0 on 2026-05-25 by localizing row permission,
|
||||
// admin access, and internal proxy responses.
|
||||
"user-facing-response-literal": 0,
|
||||
};
|
||||
|
||||
function slash(relativePath) {
|
||||
return relativePath.split(path.sep).join("/");
|
||||
}
|
||||
|
||||
function isIgnoredDirectory(name, relativePath) {
|
||||
return ignoredDirectoryNames.has(name) || ignoredDirectoryNames.has(relativePath) || name.endsWith(".cache");
|
||||
}
|
||||
|
||||
function isIgnoredFile(name, relativePath) {
|
||||
return (
|
||||
ignoredFileNames.has(name) ||
|
||||
name.endsWith(".generated.ts") ||
|
||||
name.endsWith(".generated.tsx") ||
|
||||
name.endsWith(".min.js") ||
|
||||
name.endsWith(".map") ||
|
||||
testFilePattern.test(relativePath)
|
||||
);
|
||||
}
|
||||
|
||||
async function rootExists(root, rootRelativePath) {
|
||||
try {
|
||||
await stat(path.join(root, rootRelativePath));
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error?.code === "ENOENT") return false;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function collectFiles(root, rootRelativePath) {
|
||||
const absoluteRoot = path.join(root, rootRelativePath);
|
||||
const rootStat = await stat(absoluteRoot);
|
||||
if (rootStat.isFile()) return [absoluteRoot];
|
||||
const files = [];
|
||||
async function walk(directory) {
|
||||
const entries = await readdir(directory, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const absolutePath = path.join(directory, entry.name);
|
||||
const relativePath = slash(path.relative(root, absolutePath));
|
||||
if (entry.isDirectory()) {
|
||||
if (isIgnoredDirectory(entry.name, relativePath)) continue;
|
||||
await walk(absolutePath);
|
||||
} else if (entry.isFile() && textFilePattern.test(entry.name) && !isIgnoredFile(entry.name, relativePath)) {
|
||||
files.push(absolutePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
await walk(absoluteRoot);
|
||||
return files;
|
||||
}
|
||||
|
||||
function isReviewedMatch(relativeFile, line) {
|
||||
if (reviewedIdentifierLinePatterns.some((pattern) => pattern.test(line))) {
|
||||
return true;
|
||||
}
|
||||
if (reviewedPathOnlyPatterns.some((pattern) => pattern.test(relativeFile))) {
|
||||
return true;
|
||||
}
|
||||
if (relativeFile === "apps/api/src/openapi.ts") {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
reviewedPathPatterns.some((pattern) => pattern.test(relativeFile)) &&
|
||||
(reviewedLinePatterns.some((pattern) => pattern.test(line)) || relativeFile.startsWith("packages/brand/"))
|
||||
);
|
||||
}
|
||||
|
||||
function isHistoricalMatch(relativeFile) {
|
||||
return historicalPathPatterns.some((pattern) => pattern.test(relativeFile));
|
||||
}
|
||||
|
||||
function cleanSample(line) {
|
||||
return line.trim().replace(/\s+/g, " ").slice(0, 220);
|
||||
}
|
||||
|
||||
function isCommentOnlyLine(line) {
|
||||
const trimmed = line.trim();
|
||||
return trimmed.startsWith("//") || trimmed.startsWith("{/*") || trimmed.startsWith("/*") || trimmed.startsWith("*");
|
||||
}
|
||||
|
||||
function isTypeOnlyLine(line) {
|
||||
return /=>\s*[A-Za-z_$][\w$]*<[^>]+>\s*&\s*\{/.test(line);
|
||||
}
|
||||
|
||||
export async function scanHardcodedStrings(root = repoRoot) {
|
||||
const roots = [];
|
||||
const missingRoots = [];
|
||||
for (const rootRelativePath of scanRoots) {
|
||||
if (await rootExists(root, rootRelativePath)) roots.push(rootRelativePath);
|
||||
else missingRoots.push(rootRelativePath);
|
||||
}
|
||||
|
||||
const files = (await Promise.all(roots.map((rootRelativePath) => collectFiles(root, rootRelativePath)))).flat().sort();
|
||||
const counts = Object.fromEntries(forbiddenPatterns.map(({ id }) => [id, 0]));
|
||||
const reviewedCounts = Object.fromEntries(forbiddenPatterns.map(({ id }) => [id, 0]));
|
||||
const historicalCounts = Object.fromEntries(forbiddenPatterns.map(({ id }) => [id, 0]));
|
||||
const findings = [];
|
||||
const reviewedFindings = [];
|
||||
const historicalFindings = [];
|
||||
|
||||
for (const file of files) {
|
||||
const relativeFile = slash(path.relative(root, file));
|
||||
const lines = (await readFile(file, "utf8")).split(/\r?\n/);
|
||||
lines.forEach((line, index) => {
|
||||
if (isCommentOnlyLine(line)) return;
|
||||
if (isTypeOnlyLine(line)) return;
|
||||
for (const { id, pattern } of forbiddenPatterns) {
|
||||
pattern.lastIndex = 0;
|
||||
const matches = [...line.matchAll(pattern)];
|
||||
if (matches.length === 0) continue;
|
||||
const reviewed = isReviewedMatch(relativeFile, line);
|
||||
const historical = !reviewed && isHistoricalMatch(relativeFile);
|
||||
const targetCounts = reviewed ? reviewedCounts : historical ? historicalCounts : counts;
|
||||
targetCounts[id] += matches.length;
|
||||
const sample = `${relativeFile}:${index + 1}: [${id}] ${cleanSample(line)}`;
|
||||
if (reviewed) reviewedFindings.push(sample);
|
||||
else if (historical) historicalFindings.push(sample);
|
||||
else findings.push(sample);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const totalForbidden = Object.values(counts).reduce((sum, count) => sum + count, 0);
|
||||
const totalReviewed = Object.values(reviewedCounts).reduce((sum, count) => sum + count, 0);
|
||||
const totalHistorical = Object.values(historicalCounts).reduce((sum, count) => sum + count, 0);
|
||||
return {
|
||||
roots,
|
||||
missingRoots,
|
||||
scannedFiles: files.length,
|
||||
counts,
|
||||
reviewedCounts,
|
||||
historicalCounts,
|
||||
totalForbidden,
|
||||
totalReviewed,
|
||||
totalHistorical,
|
||||
findings,
|
||||
reviewedFindings,
|
||||
historicalFindings,
|
||||
};
|
||||
}
|
||||
|
||||
export function getHistoricalBaselineDrift(result, limits = historicalBaselineLimits) {
|
||||
return Object.entries(result.historicalCounts)
|
||||
.map(([id, count]) => ({ id, count, limit: limits[id] ?? 0, overage: count - (limits[id] ?? 0) }))
|
||||
.filter((item) => item.overage > 0);
|
||||
}
|
||||
|
||||
export function renderHardcodedStringReport(result, sampleLimit = 40) {
|
||||
const historicalDrift = getHistoricalBaselineDrift(result);
|
||||
const lines = [
|
||||
"i18n Hardcoded Strings hardcoded-string guard",
|
||||
`scanned roots: ${result.roots.join(", ") || "(none)"}`,
|
||||
`missing roots skipped: ${result.missingRoots.join(", ") || "(none)"}`,
|
||||
"skipped categories: generated/build/cache/docs/dependencies/TC/test-spec artifacts",
|
||||
`scanned files: ${result.scannedFiles}`,
|
||||
...Object.entries(result.counts).map(([id, count]) => `${id}: ${count}`),
|
||||
...Object.entries(result.reviewedCounts).map(([id, count]) => `reviewed ${id}: ${count}`),
|
||||
...Object.entries(result.historicalCounts).map(([id, count]) => `historical baseline ${id}: ${count}`),
|
||||
`total forbidden matches: ${result.totalForbidden}`,
|
||||
`total reviewed allowlist matches: ${result.totalReviewed}`,
|
||||
`total historical baseline matches: ${result.totalHistorical}`,
|
||||
];
|
||||
if (historicalDrift.length > 0) {
|
||||
lines.push("historical baseline drift:", ...historicalDrift.map((item) => `${item.id}: ${item.count} exceeds baseline ${item.limit} by ${item.overage}`));
|
||||
}
|
||||
|
||||
if (result.findings.length > 0) {
|
||||
lines.push("forbidden samples:", ...result.findings.slice(0, sampleLimit));
|
||||
if (result.findings.length > sampleLimit) lines.push(`... ${result.findings.length - sampleLimit} additional forbidden matches omitted`);
|
||||
}
|
||||
if (result.reviewedFindings.length > 0) {
|
||||
lines.push("reviewed allowlist samples:", ...result.reviewedFindings.slice(0, sampleLimit));
|
||||
if (result.reviewedFindings.length > sampleLimit) lines.push(`... ${result.reviewedFindings.length - sampleLimit} additional reviewed matches omitted`);
|
||||
}
|
||||
if (result.historicalFindings.length > 0) {
|
||||
lines.push("historical baseline samples:", ...result.historicalFindings.slice(0, sampleLimit));
|
||||
if (result.historicalFindings.length > sampleLimit)
|
||||
lines.push(`... ${result.historicalFindings.length - sampleLimit} additional historical matches omitted`);
|
||||
}
|
||||
return `${lines.join("\n")}\n`;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const result = await scanHardcodedStrings(repoRoot);
|
||||
const report = renderHardcodedStringReport(result);
|
||||
if (result.missingRoots.length > 0 || result.scannedFiles === 0 || result.totalForbidden > 0 || getHistoricalBaselineDrift(result).length > 0) {
|
||||
console.error(report);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(report);
|
||||
}
|
||||
|
||||
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user