diff --git a/.changeset/lint-hide-platform-baseline.md b/.changeset/lint-hide-platform-baseline.md new file mode 100644 index 000000000..ebb8f8b24 --- /dev/null +++ b/.changeset/lint-hide-platform-baseline.md @@ -0,0 +1,5 @@ +--- +"@objectstack/cli": patch +--- + +`os lint` no longer buries the user's own signal under the platform i18n baseline. A fresh scaffold reported 800+ `i18n/missing-metadataForm` errors — translation keys for platform built-in metadata forms (email_template, …) that the platform packages already ship at runtime. Those are now hidden by default and folded into one summary line (`platform built-ins: N i18n issue(s) hidden`); pass `--include-platform` to audit them, and read `hiddenPlatform` in `--json` output. User-authored metadata coverage is reported unchanged. diff --git a/packages/cli/src/commands/lint.ts b/packages/cli/src/commands/lint.ts index d2701ce49..c032947cc 100644 --- a/packages/cli/src/commands/lint.ts +++ b/packages/cli/src/commands/lint.ts @@ -6,7 +6,7 @@ import { bundleRequire } from 'bundle-require'; import { normalizeStackInput } from '@objectstack/spec'; import { PROTOCOL_MAJOR } from '@objectstack/spec/kernel'; import { loadConfig, BUNDLE_REQUIRE_EXTERNALS } from '../utils/config.js'; -import { computeI18nCoverage } from '../utils/i18n-coverage.js'; +import { computeI18nCoverage, type CoverageIssue } from '../utils/i18n-coverage.js'; import { lintDataModel } from '../lint/data-model-rules.js'; import { validateWidgetBindings } from '@objectstack/lint'; import { validateRecordTitle, validateSemanticRoles, validateCapabilityReferences, validateSecurityPosture, validateApprovalApprovers } from '@objectstack/lint'; @@ -36,6 +36,34 @@ interface LintIssue { fix?: string; } +// Fold i18n coverage issues into lint issues, separating the platform +// baseline from the user's own metadata. `metadataForm` issues come from +// walking the static platform registries (DEFAULT_METADATA_TYPE_REGISTRY + +// METADATA_FORM_REGISTRY) — ~850 Studio-form keys that the platform packages +// already translate at runtime. On a fresh project they would drown every +// user-authored signal (15.1 third-party eval: 848/848 errors were platform +// noise), so they are hidden unless explicitly requested. +export function foldCoverageIssues( + coverageIssues: CoverageIssue[], + includePlatform: boolean, +): { folded: LintIssue[]; hiddenPlatform: number } { + const folded: LintIssue[] = []; + let hiddenPlatform = 0; + for (const c of coverageIssues) { + if (!includePlatform && c.source === 'metadataForm') { + hiddenPlatform++; + continue; + } + folded.push({ + severity: c.severity === 'error' ? 'error' : 'warning', + rule: `i18n/missing-${c.source}`, + message: c.message, + path: `translations.${c.locale}.${c.key}`, + }); + } + return { folded, hiddenPlatform }; +} + // ─── Rules ────────────────────────────────────────────────────────── const SNAKE_CASE_RE = /^[a-z][a-z0-9_]*$/; @@ -441,6 +469,10 @@ export default class Lint extends Command { default: 75, }), 'skip-i18n': Flags.boolean({ description: 'Skip translation coverage checks' }), + 'include-platform': Flags.boolean({ + description: + 'Also report i18n coverage for platform built-in metadata forms (hidden by default — the platform packages ship those translations)', + }), 'i18n-strict': Flags.boolean({ description: 'Treat missing translations in non-default locales as errors', }), @@ -486,19 +518,18 @@ export default class Lint extends Command { } // ── Translation coverage ── + let hiddenPlatform = 0; if (!flags['skip-i18n']) { const coverage = computeI18nCoverage(normalized, { defaultLocale: flags['default-locale'], strict: flags['i18n-strict'], }); - for (const c of coverage.issues) { - issues.push({ - severity: c.severity === 'error' ? 'error' : 'warning', - rule: `i18n/missing-${c.source}`, - message: c.message, - path: `translations.${c.locale}.${c.key}`, - }); - } + const { folded, hiddenPlatform: hidden } = foldCoverageIssues( + coverage.issues, + flags['include-platform'] ?? false, + ); + hiddenPlatform = hidden; + issues.push(...folded); } // Metadata-quality score (the lint rubric expressed as 0–100). @@ -515,6 +546,7 @@ export default class Lint extends Command { errors: errors.length, warnings: warnings.length, suggestions: suggestions.length, + ...(hiddenPlatform > 0 ? { hiddenPlatform } : {}), ...(score ? { score: score.score, grade: score.grade } : {}), issues, duration: timer.elapsed(), @@ -525,8 +557,19 @@ export default class Lint extends Command { console.log(''); + const printHiddenPlatform = () => { + if (hiddenPlatform > 0) { + console.log( + chalk.dim( + ` platform built-ins: ${hiddenPlatform} i18n issue(s) hidden — rerun with --include-platform to audit them`, + ), + ); + } + }; + if (issues.length === 0) { printSuccess(`All checks passed ${chalk.dim(`(${timer.display()})`)}`); + printHiddenPlatform(); if (score) this.printScore(score); console.log(''); return; @@ -578,6 +621,7 @@ export default class Lint extends Command { if (warnings.length > 0) parts.push(chalk.yellow(`${warnings.length} warning(s)`)); if (suggestions.length > 0) parts.push(chalk.blue(`${suggestions.length} suggestion(s)`)); console.log(` ${parts.join(', ')} ${chalk.dim(`(${timer.display()})`)}`); + printHiddenPlatform(); if (score) this.printScore(score); diff --git a/packages/cli/test/lint-platform-fold.test.ts b/packages/cli/test/lint-platform-fold.test.ts new file mode 100644 index 000000000..e25bd6a7e --- /dev/null +++ b/packages/cli/test/lint-platform-fold.test.ts @@ -0,0 +1,63 @@ +// Copyright (c) 2026 ObjectStack contributors. Apache-2.0 license. +// +// `os lint` platform-noise fold (15.1 third-party eval): a fresh scaffold +// reported 848 errors, all `i18n/missing-metadataForm` — keys expected by the +// static platform registries and already translated by the platform packages +// at runtime. They must not drown the user's own i18n signal: hidden by +// default, surfaced only under --include-platform, always counted. + +import { describe, it, expect } from 'vitest'; +import { foldCoverageIssues } from '../src/commands/lint'; +import type { CoverageIssue } from '../src/utils/i18n-coverage'; + +const userIssue: CoverageIssue = { + severity: 'error', + locale: 'en', + key: 'objects.blank_note.label', + message: "Missing en translation for object 'blank_note' label", + source: 'object', +}; + +const platformIssue: CoverageIssue = { + severity: 'error', + locale: 'en', + key: 'metadataForms.email_template.fields.subject.label', + message: 'Missing en translation for metadata form email_template', + source: 'metadataForm', +}; + +describe('foldCoverageIssues', () => { + it('hides platform metadata-form issues by default and counts them', () => { + const { folded, hiddenPlatform } = foldCoverageIssues( + [userIssue, platformIssue, { ...platformIssue, locale: 'zh-CN', severity: 'warning' }], + false, + ); + expect(hiddenPlatform).toBe(2); + expect(folded).toHaveLength(1); + expect(folded[0]).toMatchObject({ + severity: 'error', + rule: 'i18n/missing-object', + path: 'translations.en.objects.blank_note.label', + }); + }); + + it('keeps the user signal intact when nothing is platform-sourced', () => { + const { folded, hiddenPlatform } = foldCoverageIssues([userIssue], false); + expect(hiddenPlatform).toBe(0); + expect(folded).toHaveLength(1); + }); + + it('surfaces everything under --include-platform', () => { + const { folded, hiddenPlatform } = foldCoverageIssues( + [userIssue, platformIssue], + true, + ); + expect(hiddenPlatform).toBe(0); + expect(folded).toHaveLength(2); + expect(folded.map((i) => i.rule).sort()).toEqual([ + 'i18n/missing-metadataForm', + 'i18n/missing-object', + ]); + expect(folded[1].severity).toBe('error'); + }); +});