Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/lint-hide-platform-baseline.md
Original file line number Diff line number Diff line change
@@ -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.
62 changes: 53 additions & 9 deletions packages/cli/src/commands/lint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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_]*$/;
Expand Down Expand Up @@ -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',
}),
Expand Down Expand Up @@ -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).
Expand All @@ -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(),
Expand All @@ -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;
Expand Down Expand Up @@ -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);

Expand Down
63 changes: 63 additions & 0 deletions packages/cli/test/lint-platform-fold.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});