mustflow 2.58.0 → 2.59.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/README.md CHANGED
@@ -122,6 +122,7 @@ mustflow installs and validates an agent workflow for user projects.
122
122
  - Checks installation health and configuration structure with `mf check` and `mf doctor`.
123
123
  - Reports host adapter compatibility with `mf adapters status` without generating host-specific files or treating them as command authority.
124
124
  - Classifies changed files, public surfaces, and validation reasons with `mf classify`.
125
+ - Inspects changed files for quality-gaming patterns such as line stuffing, suppressions, test bypass markers, type escapes, and placeholder implementations with `mf quality check`.
125
126
  - Prints execution-free verification plans with `mf verify --plan-only --json`, including a machine-readable verification decision graph and read-only local-index lock explanations when available.
126
127
  - Runs only allowed one-shot commands within a timeout via `mf run <intent>` or `mf verify` when the selected intent is runnable.
127
128
  - Writes command receipts under `.mustflow/state/runs/run-*` and atomically updates `.mustflow/state/runs/latest.json`.
@@ -271,6 +272,8 @@ mf run mustflow_update_apply
271
272
  | `mf context --json` | Print read order, command rules, available capabilities, and recent run summary as JSON. |
272
273
  | `mf map --stdout` | Print the current mustflow root map to stdout. |
273
274
  | `mf map --write` | Create or update `REPO_MAP.md`. |
275
+ | `mf quality check` | Inspect changed files for quality-gaming patterns without writing files. |
276
+ | `mf quality check --all` | Inspect every tracked text file for quality-gaming patterns. |
274
277
  | `mf run <intent>` | Run an allowed one-shot command. |
275
278
  | `mf run <intent> --wait` | Wait for conflicting active run locks before executing the command. |
276
279
  | `mf run <intent> --dry-run --json` | Preview whether an intent is runnable and what command metadata would be used, without executing it. |
@@ -0,0 +1,89 @@
1
+ import { printUsageError, renderHelp } from '../lib/cli-output.js';
2
+ import { t } from '../lib/i18n.js';
3
+ import { formatCliOptionParseError, hasCliOptionToken, hasParsedCliOption, parseCliOptions, } from '../lib/option-parser.js';
4
+ import { resolveMustflowRoot } from '../lib/project-root.js';
5
+ import { inspectQualityGaming } from '../../core/quality-gaming.js';
6
+ const QUALITY_OPTIONS = [
7
+ { name: '--json', kind: 'boolean' },
8
+ { name: '--all', kind: 'boolean' },
9
+ ];
10
+ export function getQualityHelp(lang = 'en') {
11
+ return renderHelp({
12
+ usage: 'mf quality <check> [options]',
13
+ summary: t(lang, 'quality.help.summary'),
14
+ options: [
15
+ { label: '--json', description: t(lang, 'cli.option.json') },
16
+ { label: '--all', description: t(lang, 'quality.help.option.all') },
17
+ { label: '-h, --help', description: t(lang, 'cli.option.help') },
18
+ ],
19
+ examples: ['mf quality check', 'mf quality check --json', 'mf quality check --all'],
20
+ exitCodes: [
21
+ { label: '0', description: t(lang, 'quality.help.exit.ok') },
22
+ { label: '1', description: t(lang, 'quality.help.exit.fail') },
23
+ ],
24
+ }, lang);
25
+ }
26
+ function parseQualityOptions(args, lang) {
27
+ const [action, ...rest] = args;
28
+ const parsed = parseCliOptions(rest, QUALITY_OPTIONS, { allowPositionals: true });
29
+ const json = hasParsedCliOption(parsed, '--json');
30
+ const all = hasParsedCliOption(parsed, '--all');
31
+ if (action !== 'check') {
32
+ return {
33
+ action: 'check',
34
+ json,
35
+ all,
36
+ error: action ? t(lang, 'quality.error.unknownAction', { action }) : t(lang, 'quality.error.missingAction'),
37
+ };
38
+ }
39
+ if (parsed.error) {
40
+ return { action, json, all, error: formatCliOptionParseError(parsed.error, lang) };
41
+ }
42
+ return { action, json, all };
43
+ }
44
+ function renderQualitySummary(report, lang) {
45
+ const lines = [
46
+ t(lang, 'quality.title'),
47
+ `${t(lang, 'label.mode')}: ${report.mode}`,
48
+ `${t(lang, 'label.scope')}: ${report.scope}`,
49
+ `${t(lang, 'quality.label.checkedFiles')}: ${report.checked_files}`,
50
+ `${t(lang, 'quality.label.riskCount')}: ${report.risk_count}`,
51
+ ];
52
+ if (report.issues.length > 0) {
53
+ lines.push(t(lang, 'quality.label.issues'), ...report.issues.map((issue) => `- ${issue}`));
54
+ }
55
+ if (report.risky_files.length > 0) {
56
+ lines.push(t(lang, 'quality.label.riskyFiles'));
57
+ for (const file of report.risky_files) {
58
+ lines.push(`- ${file.path} (${file.risk_count})`);
59
+ for (const risk of file.risks) {
60
+ const line = risk.line === null ? '' : `:${risk.line}`;
61
+ lines.push(` - ${risk.code}${line} [${risk.severity}] ${risk.detail}`);
62
+ }
63
+ }
64
+ }
65
+ if (report.risky_files.length === 0 && report.issues.length === 0) {
66
+ lines.push(t(lang, 'quality.clean'));
67
+ }
68
+ return lines.join('\n');
69
+ }
70
+ export function runQuality(args, reporter, lang = 'en') {
71
+ if (hasCliOptionToken(args, '--help', ['-h'])) {
72
+ reporter.stdout(getQualityHelp(lang));
73
+ return 0;
74
+ }
75
+ const options = parseQualityOptions(args, lang);
76
+ if (options.error) {
77
+ printUsageError(reporter, options.error, 'mf quality --help', getQualityHelp(lang), lang);
78
+ return 1;
79
+ }
80
+ const report = inspectQualityGaming(resolveMustflowRoot(), {
81
+ scope: options.all ? 'tracked' : 'changed',
82
+ });
83
+ if (options.json) {
84
+ reporter.stdout(JSON.stringify(report, null, 2));
85
+ return report.ok ? 0 : 1;
86
+ }
87
+ reporter.stdout(renderQualitySummary(report, lang));
88
+ return report.ok ? 0 : 1;
89
+ }
@@ -3,6 +3,7 @@ import { printUsageError, renderCliError, renderHelp } from '../lib/cli-output.j
3
3
  import { writeUtf8FileInsideWithoutSymlinks } from '../lib/filesystem.js';
4
4
  import { isRecord } from '../lib/command-contract.js';
5
5
  import { t } from '../lib/i18n.js';
6
+ import { checkNpmPackageExists } from '../lib/npm-version-check.js';
6
7
  import { formatCliOptionParseError, getParsedCliStringOption, hasCliOptionToken, hasParsedCliOption, parseCliOptions, } from '../lib/option-parser.js';
7
8
  import { resolveMustflowRoot } from '../lib/project-root.js';
8
9
  import { readMustflowTomlFile } from '../lib/toml.js';
@@ -14,6 +15,7 @@ const TECH_OPTIONS = [
14
15
  { name: '--status', kind: 'string' },
15
16
  { name: '--ecosystem', kind: 'string' },
16
17
  { name: '--package', kind: 'string' },
18
+ { name: '--verify', kind: 'boolean' },
17
19
  { name: '--why', kind: 'string' },
18
20
  { name: '--constraint', kind: 'string' },
19
21
  ];
@@ -40,13 +42,14 @@ export function getTechHelp(lang = 'en') {
40
42
  { label: '--status <status>', description: `Preference status: ${TECHNOLOGY_STATUSES.join(', ')}` },
41
43
  { label: '--ecosystem <ecosystem>', description: 'Package ecosystem or platform, such as npm, cargo, pip, go, or deno' },
42
44
  { label: '--package <package>', description: 'Package name to associate with the preference. Repeatable.' },
45
+ { label: '--verify', description: 'Verify listed npm packages exist before writing the preference' },
43
46
  { label: '--why <text>', description: 'Short rationale for the preference' },
44
47
  { label: '--constraint <text>', description: 'Guardrail agents must keep in mind. Repeatable.' },
45
48
  { label: '-h, --help', description: t(lang, 'cli.option.help') },
46
49
  ],
47
50
  examples: [
48
51
  'mf tech list',
49
- 'mf tech add framework nextjs --scope frontend --ecosystem npm --package next --package react --why "Preferred React app framework"',
52
+ 'mf tech add framework nextjs --scope frontend --ecosystem npm --package next --package react --verify --why "Preferred React app framework"',
50
53
  'mf tech add language rust --scope backend --status preferred --why "Use for correctness-critical engines"',
51
54
  'mf tech add library jquery --scope frontend --status avoid --why "Avoid new usage"',
52
55
  'mf tech suggest --scope frontend',
@@ -71,6 +74,7 @@ function parseTechOptions(args, lang) {
71
74
  status: null,
72
75
  ecosystem: null,
73
76
  packages: [],
77
+ verify: false,
74
78
  why: null,
75
79
  constraints: [],
76
80
  error: actionToken ? `Unknown tech action: ${actionToken}` : 'Specify a tech action: list, add, remove, or suggest',
@@ -87,6 +91,7 @@ function parseTechOptions(args, lang) {
87
91
  status: null,
88
92
  ecosystem: null,
89
93
  packages: [],
94
+ verify: hasParsedCliOption(parsed, '--verify'),
90
95
  why: null,
91
96
  constraints: [],
92
97
  error: formatCliOptionParseError(parsed.error, lang),
@@ -109,6 +114,7 @@ function parseTechOptions(args, lang) {
109
114
  status,
110
115
  ecosystem: getParsedCliStringOption(parsed, '--ecosystem'),
111
116
  packages: getRepeatedStringOptions(parsed.occurrences, '--package'),
117
+ verify: hasParsedCliOption(parsed, '--verify'),
112
118
  why: getParsedCliStringOption(parsed, '--why'),
113
119
  constraints: getRepeatedStringOptions(parsed.occurrences, '--constraint'),
114
120
  };
@@ -123,6 +129,7 @@ function invalidParsed(action, positionals, json, error) {
123
129
  status: null,
124
130
  ecosystem: null,
125
131
  packages: [],
132
+ verify: false,
126
133
  why: null,
127
134
  constraints: [],
128
135
  error,
@@ -239,7 +246,44 @@ function findExistingIndex(preferences, candidate) {
239
246
  return preference.kind === candidate.kind && normalizeTechnologyKey(preference.name) === normalizeTechnologyKey(candidate.name);
240
247
  });
241
248
  }
242
- function runAdd(projectRoot, options, reporter) {
249
+ async function verifyNpmPackages(options, reporter) {
250
+ if (!options.verify) {
251
+ return [];
252
+ }
253
+ if (options.action !== 'add') {
254
+ reporter.stderr(renderCliError('--verify is only supported with mf tech add', 'mf tech --help'));
255
+ return null;
256
+ }
257
+ if (options.packages.length === 0) {
258
+ reporter.stderr(renderCliError('--verify requires at least one --package value', 'mf tech --help'));
259
+ return null;
260
+ }
261
+ if (options.ecosystem !== 'npm') {
262
+ reporter.stderr(renderCliError('--verify currently supports --ecosystem npm only', 'mf tech --help'));
263
+ return null;
264
+ }
265
+ const checks = [];
266
+ for (const packageName of options.packages) {
267
+ try {
268
+ const check = await checkNpmPackageExists(packageName);
269
+ if (!check.exists) {
270
+ reporter.stderr(renderCliError(`npm package not found: ${packageName}`, 'mf tech --help'));
271
+ return null;
272
+ }
273
+ checks.push(check);
274
+ }
275
+ catch (error) {
276
+ const message = error instanceof Error ? error.message : String(error);
277
+ reporter.stderr(renderCliError(`Could not verify npm package ${packageName}: ${message}`, 'mf tech --help'));
278
+ return null;
279
+ }
280
+ }
281
+ return checks;
282
+ }
283
+ function renderVerifiedPackageNames(checks) {
284
+ return checks.map((check) => check.resolvedName ?? check.packageName).join(', ');
285
+ }
286
+ async function runAdd(projectRoot, options, reporter) {
243
287
  const [kindToken, nameToken] = options.positionals;
244
288
  if (!isTechnologyKind(kindToken ?? null)) {
245
289
  reporter.stderr(renderCliError('Missing or unsupported technology kind', 'mf tech --help'));
@@ -277,6 +321,10 @@ function runAdd(projectRoot, options, reporter) {
277
321
  const preferences = [...file.preferences];
278
322
  const existingIndex = findExistingIndex(preferences, candidate);
279
323
  const action = existingIndex === -1 ? 'created' : 'updated';
324
+ const verifiedPackages = await verifyNpmPackages(options, reporter);
325
+ if (verifiedPackages === null) {
326
+ return 1;
327
+ }
280
328
  if (existingIndex === -1) {
281
329
  preferences.push(candidate);
282
330
  }
@@ -285,9 +333,17 @@ function runAdd(projectRoot, options, reporter) {
285
333
  }
286
334
  writeTechnologyPreferences(projectRoot, preferences);
287
335
  if (options.json) {
288
- reporter.stdout(JSON.stringify({ action, preference: candidate, path: TECHNOLOGY_CONFIG_RELATIVE_PATH }, null, 2));
336
+ reporter.stdout(JSON.stringify({
337
+ action,
338
+ preference: candidate,
339
+ path: TECHNOLOGY_CONFIG_RELATIVE_PATH,
340
+ verified_packages: verifiedPackages,
341
+ }, null, 2));
289
342
  return 0;
290
343
  }
344
+ if (verifiedPackages.length > 0) {
345
+ reporter.stdout(`Verified npm packages: ${renderVerifiedPackageNames(verifiedPackages)}`);
346
+ }
291
347
  reporter.stdout(`${action === 'created' ? 'Created' : 'Updated'} ${candidate.id} in ${TECHNOLOGY_CONFIG_RELATIVE_PATH}`);
292
348
  return 0;
293
349
  }
@@ -322,7 +378,7 @@ function runRemove(projectRoot, options, reporter) {
322
378
  reporter.stdout(`Removed ${removed.id} from ${TECHNOLOGY_CONFIG_RELATIVE_PATH}`);
323
379
  return 0;
324
380
  }
325
- export function runTech(args, reporter, lang = 'en') {
381
+ export async function runTech(args, reporter, lang = 'en') {
326
382
  if (hasCliOptionToken(args, '--help', ['-h'])) {
327
383
  reporter.stdout(getTechHelp(lang));
328
384
  return 0;
@@ -39,6 +39,7 @@ export const enMessages = {
39
39
  "command.upgrade.summary": "Check the package version and safely update installed workflow files",
40
40
  "command.map.summary": "Generate REPO_MAP.md",
41
41
  "command.lineEndings.summary": "Inspect and normalize line-ending policy",
42
+ "command.quality.summary": "Inspect changed files for quality-gaming patterns",
42
43
  "command.run.summary": "Run a configured oneshot command",
43
44
  "command.context.summary": "Print machine-readable agent context",
44
45
  "command.tech.summary": "Manage technology preferences for agents",
@@ -731,6 +732,19 @@ Read these files before working:
731
732
  "lineEndings.error.unknownAction": "Unknown line-endings action: {action}",
732
733
  "lineEndings.error.checkModeOption": "check does not accept --apply or --dry-run",
733
734
  "lineEndings.error.conflictingModes": "Cannot combine --apply and --dry-run",
735
+ "quality.help.summary": "Inspect changed files for quality-gaming patterns such as line stuffing, validation suppressions, " +
736
+ "test bypass markers, type escapes, and placeholder implementations.",
737
+ "quality.help.option.all": "Inspect every tracked text file instead of only changed files",
738
+ "quality.help.exit.ok": "Quality-gaming risks were inspected and no risk was found",
739
+ "quality.help.exit.fail": "Quality-gaming risks, repository issues, or invalid input were found",
740
+ "quality.title": "mustflow quality check",
741
+ "quality.label.checkedFiles": "Checked files",
742
+ "quality.label.riskCount": "Risks",
743
+ "quality.label.riskyFiles": "Risky files",
744
+ "quality.label.issues": "Issues",
745
+ "quality.clean": "No quality-gaming risks found.",
746
+ "quality.error.missingAction": "Specify a quality action: check",
747
+ "quality.error.unknownAction": "Unknown quality action: {action}",
734
748
  "run.help.summary": "Run a configured oneshot command from .mustflow/config/commands.toml.",
735
749
  "run.help.option.dryRun": "Print a non-executing command plan",
736
750
  "run.help.option.planOnly": "Alias for --dry-run",
@@ -39,6 +39,7 @@ export const esMessages = {
39
39
  "command.upgrade.summary": "Comprueba la versión del paquete y actualiza con seguridad los archivos de flujo instalados",
40
40
  "command.map.summary": "Genera REPO_MAP.md",
41
41
  "command.lineEndings.summary": "Inspecciona y normaliza la política de finales de línea",
42
+ "command.quality.summary": "Inspect changed files for quality-gaming patterns",
42
43
  "command.run.summary": "Ejecuta un comando configurado de una sola ejecución",
43
44
  "command.context.summary": "Imprime contexto de agente legible por máquinas",
44
45
  "command.tech.summary": "Gestiona preferencias tecnológicas para agentes",
@@ -731,6 +732,19 @@ Lee estos archivos antes de trabajar:
731
732
  "lineEndings.error.unknownAction": "Unknown line-endings action: {action}",
732
733
  "lineEndings.error.checkModeOption": "check does not accept --apply or --dry-run",
733
734
  "lineEndings.error.conflictingModes": "Cannot combine --apply and --dry-run",
735
+ "quality.help.summary": "Inspect changed files for quality-gaming patterns such as line stuffing, validation suppressions, " +
736
+ "test bypass markers, type escapes, and placeholder implementations.",
737
+ "quality.help.option.all": "Inspect every tracked text file instead of only changed files",
738
+ "quality.help.exit.ok": "Quality-gaming risks were inspected and no risk was found",
739
+ "quality.help.exit.fail": "Quality-gaming risks, repository issues, or invalid input were found",
740
+ "quality.title": "mustflow quality check",
741
+ "quality.label.checkedFiles": "Checked files",
742
+ "quality.label.riskCount": "Risks",
743
+ "quality.label.riskyFiles": "Risky files",
744
+ "quality.label.issues": "Issues",
745
+ "quality.clean": "No quality-gaming risks found.",
746
+ "quality.error.missingAction": "Specify a quality action: check",
747
+ "quality.error.unknownAction": "Unknown quality action: {action}",
734
748
  "run.help.summary": "Ejecuta un comando configurado de una sola ejecución desde .mustflow/config/commands.toml.",
735
749
  "run.help.option.dryRun": "Imprime un plan de comando sin ejecutarlo",
736
750
  "run.help.option.planOnly": "Alias de --dry-run",
@@ -39,6 +39,7 @@ export const frMessages = {
39
39
  "command.upgrade.summary": "Vérifie la version du paquet et met à jour en sécurité les fichiers de workflow installés",
40
40
  "command.map.summary": "Génère REPO_MAP.md",
41
41
  "command.lineEndings.summary": "Inspecte et normalise la politique de fins de ligne",
42
+ "command.quality.summary": "Inspect changed files for quality-gaming patterns",
42
43
  "command.run.summary": "Exécute une commande configurée à exécution unique",
43
44
  "command.context.summary": "Imprime le contexte d'agent lisible par machine",
44
45
  "command.tech.summary": "Gère les préférences technologiques pour les agents",
@@ -731,6 +732,19 @@ Lisez ces fichiers avant de travailler :
731
732
  "lineEndings.error.unknownAction": "Unknown line-endings action: {action}",
732
733
  "lineEndings.error.checkModeOption": "check does not accept --apply or --dry-run",
733
734
  "lineEndings.error.conflictingModes": "Cannot combine --apply and --dry-run",
735
+ "quality.help.summary": "Inspect changed files for quality-gaming patterns such as line stuffing, validation suppressions, " +
736
+ "test bypass markers, type escapes, and placeholder implementations.",
737
+ "quality.help.option.all": "Inspect every tracked text file instead of only changed files",
738
+ "quality.help.exit.ok": "Quality-gaming risks were inspected and no risk was found",
739
+ "quality.help.exit.fail": "Quality-gaming risks, repository issues, or invalid input were found",
740
+ "quality.title": "mustflow quality check",
741
+ "quality.label.checkedFiles": "Checked files",
742
+ "quality.label.riskCount": "Risks",
743
+ "quality.label.riskyFiles": "Risky files",
744
+ "quality.label.issues": "Issues",
745
+ "quality.clean": "No quality-gaming risks found.",
746
+ "quality.error.missingAction": "Specify a quality action: check",
747
+ "quality.error.unknownAction": "Unknown quality action: {action}",
734
748
  "run.help.summary": "Exécute une commande configurée à exécution unique depuis .mustflow/config/commands.toml.",
735
749
  "run.help.option.dryRun": "Imprime un plan de commande sans l'exécuter",
736
750
  "run.help.option.planOnly": "Alias de --dry-run",
@@ -39,6 +39,7 @@ export const hiMessages = {
39
39
  "command.upgrade.summary": "Package version जाँचें और installed workflow files सुरक्षित रूप से update करें",
40
40
  "command.map.summary": "REPO_MAP.md बनाएँ",
41
41
  "command.lineEndings.summary": "लाइन-एंडिंग नीति की जाँच और सामान्यीकरण करें",
42
+ "command.quality.summary": "Inspect changed files for quality-gaming patterns",
42
43
  "command.run.summary": "कॉन्फ़िगर की गई एक-बार चलने वाली कमांड चलाएँ",
43
44
  "command.context.summary": "मशीन-पठनीय एजेंट संदर्भ प्रिंट करें",
44
45
  "command.tech.summary": "एजेंटों के लिए technology preferences प्रबंधित करें",
@@ -731,6 +732,19 @@ export const hiMessages = {
731
732
  "lineEndings.error.unknownAction": "Unknown line-endings action: {action}",
732
733
  "lineEndings.error.checkModeOption": "check does not accept --apply or --dry-run",
733
734
  "lineEndings.error.conflictingModes": "Cannot combine --apply and --dry-run",
735
+ "quality.help.summary": "Inspect changed files for quality-gaming patterns such as line stuffing, validation suppressions, " +
736
+ "test bypass markers, type escapes, and placeholder implementations.",
737
+ "quality.help.option.all": "Inspect every tracked text file instead of only changed files",
738
+ "quality.help.exit.ok": "Quality-gaming risks were inspected and no risk was found",
739
+ "quality.help.exit.fail": "Quality-gaming risks, repository issues, or invalid input were found",
740
+ "quality.title": "mustflow quality check",
741
+ "quality.label.checkedFiles": "Checked files",
742
+ "quality.label.riskCount": "Risks",
743
+ "quality.label.riskyFiles": "Risky files",
744
+ "quality.label.issues": "Issues",
745
+ "quality.clean": "No quality-gaming risks found.",
746
+ "quality.error.missingAction": "Specify a quality action: check",
747
+ "quality.error.unknownAction": "Unknown quality action: {action}",
734
748
  "run.help.summary": ".mustflow/config/commands.toml से कॉन्फ़िगर की गई एक-बार चलने वाली कमांड चलाएँ।",
735
749
  "run.help.option.dryRun": "कमांड चलाए बिना उसका plan प्रिंट करें",
736
750
  "run.help.option.planOnly": "--dry-run का alias",
@@ -39,6 +39,7 @@ export const koMessages = {
39
39
  "command.upgrade.summary": "패키지 버전을 확인하고 설치된 워크플로우 파일을 안전하게 갱신합니다",
40
40
  "command.map.summary": "REPO_MAP.md를 작성합니다",
41
41
  "command.lineEndings.summary": "줄바꿈 정책을 검사하고 정규화합니다",
42
+ "command.quality.summary": "변경 파일의 품질 지표 꼼수를 검사합니다",
42
43
  "command.run.summary": "설정된 일회성 명령을 실행합니다",
43
44
  "command.context.summary": "에이전트 작업 맥락을 출력합니다",
44
45
  "command.tech.summary": "에이전트용 기술 선호를 관리합니다",
@@ -731,6 +732,19 @@ export const koMessages = {
731
732
  "lineEndings.error.unknownAction": "알 수 없는 line-endings 작업: {action}",
732
733
  "lineEndings.error.checkModeOption": "check에는 --apply 또는 --dry-run을 사용할 수 없습니다",
733
734
  "lineEndings.error.conflictingModes": "--apply와 --dry-run은 함께 사용할 수 없습니다",
735
+ "quality.help.summary": "긴 줄 몰아넣기, 검증 억제, 테스트 우회 표시, 타입 회피, 미완성 구현 같은 " +
736
+ "품질 지표 꼼수를 변경 파일에서 검사합니다.",
737
+ "quality.help.option.all": "변경 파일만이 아니라 추적 중인 모든 텍스트 파일을 검사합니다",
738
+ "quality.help.exit.ok": "품질 지표 꼼수를 검사했고 위험이 발견되지 않았습니다",
739
+ "quality.help.exit.fail": "품질 지표 꼼수, 저장소 문제, 또는 잘못된 입력이 발견되었습니다",
740
+ "quality.title": "mustflow quality check",
741
+ "quality.label.checkedFiles": "검사한 파일",
742
+ "quality.label.riskCount": "위험",
743
+ "quality.label.riskyFiles": "위험 파일",
744
+ "quality.label.issues": "문제",
745
+ "quality.clean": "품질 지표 꼼수가 발견되지 않았습니다.",
746
+ "quality.error.missingAction": "quality 작업을 지정하세요: check",
747
+ "quality.error.unknownAction": "알 수 없는 quality 작업: {action}",
734
748
  "run.help.summary": ".mustflow/config/commands.toml에 설정된 일회성 명령을 실행합니다.",
735
749
  "run.help.option.dryRun": "실행하지 않고 명령 계획을 출력합니다",
736
750
  "run.help.option.planOnly": "--dry-run과 같은 동작입니다",
@@ -39,6 +39,7 @@ export const zhMessages = {
39
39
  "command.upgrade.summary": "检查包版本并安全更新已安装的工作流文件",
40
40
  "command.map.summary": "生成 REPO_MAP.md",
41
41
  "command.lineEndings.summary": "检查并规范化换行符策略",
42
+ "command.quality.summary": "Inspect changed files for quality-gaming patterns",
42
43
  "command.run.summary": "运行已配置的一次性命令",
43
44
  "command.context.summary": "输出机器可读的代理上下文",
44
45
  "command.tech.summary": "管理代理使用的技术偏好",
@@ -731,6 +732,19 @@ export const zhMessages = {
731
732
  "lineEndings.error.unknownAction": "Unknown line-endings action: {action}",
732
733
  "lineEndings.error.checkModeOption": "check does not accept --apply or --dry-run",
733
734
  "lineEndings.error.conflictingModes": "Cannot combine --apply and --dry-run",
735
+ "quality.help.summary": "Inspect changed files for quality-gaming patterns such as line stuffing, validation suppressions, " +
736
+ "test bypass markers, type escapes, and placeholder implementations.",
737
+ "quality.help.option.all": "Inspect every tracked text file instead of only changed files",
738
+ "quality.help.exit.ok": "Quality-gaming risks were inspected and no risk was found",
739
+ "quality.help.exit.fail": "Quality-gaming risks, repository issues, or invalid input were found",
740
+ "quality.title": "mustflow quality check",
741
+ "quality.label.checkedFiles": "Checked files",
742
+ "quality.label.riskCount": "Risks",
743
+ "quality.label.riskyFiles": "Risky files",
744
+ "quality.label.issues": "Issues",
745
+ "quality.clean": "No quality-gaming risks found.",
746
+ "quality.error.missingAction": "Specify a quality action: check",
747
+ "quality.error.unknownAction": "Unknown quality action: {action}",
734
748
  "run.help.summary": "从 .mustflow/config/commands.toml 运行已配置的一次性命令。",
735
749
  "run.help.option.dryRun": "输出命令计划但不执行",
736
750
  "run.help.option.planOnly": "--dry-run 的别名",
package/dist/cli/index.js CHANGED
@@ -53,6 +53,7 @@ function getTopLevelHelp(lang) {
53
53
  'mf verify --changed --plan-only --json',
54
54
  'mf verify --reason code_change',
55
55
  'mf line-endings check',
56
+ 'mf quality check',
56
57
  'mf version --check',
57
58
  'mf version-sources --json',
58
59
  ],
@@ -89,6 +89,12 @@ export const COMMAND_DEFINITIONS = [
89
89
  summaryKey: 'command.lineEndings.summary',
90
90
  loadRunner: async () => (await import('../commands/line-endings.js')).runLineEndings,
91
91
  },
92
+ {
93
+ id: 'quality',
94
+ usage: 'mf quality',
95
+ summaryKey: 'command.quality.summary',
96
+ loadRunner: async () => (await import('../commands/quality.js')).runQuality,
97
+ },
92
98
  {
93
99
  id: 'run',
94
100
  usage: 'mf run',
@@ -150,6 +150,42 @@ function buildLatestPackageUrl(registryUrl, packageName) {
150
150
  : encodeURIComponent(packageName);
151
151
  return `${trimmedRegistryUrl}/${encodedPackageName}/latest`;
152
152
  }
153
+ function buildPackageMetadataUrl(registryUrl, packageName) {
154
+ const trimmedRegistryUrl = registryUrl.replace(/\/+$/u, '');
155
+ const encodedPackageName = packageName.startsWith('@')
156
+ ? `@${encodeURIComponent(packageName.slice(1))}`
157
+ : encodeURIComponent(packageName);
158
+ return `${trimmedRegistryUrl}/${encodedPackageName}`;
159
+ }
160
+ export async function checkNpmPackageExists(packageName) {
161
+ const registryUrl = getRegistryUrl();
162
+ const response = await fetch(buildPackageMetadataUrl(registryUrl, packageName), {
163
+ headers: { accept: 'application/json' },
164
+ signal: AbortSignal.timeout(getTimeoutMs()),
165
+ });
166
+ if (response.status === 404) {
167
+ return {
168
+ packageName,
169
+ registryUrl,
170
+ exists: false,
171
+ resolvedName: null,
172
+ };
173
+ }
174
+ if (!response.ok) {
175
+ throw new Error(`npm registry returned HTTP ${response.status}`);
176
+ }
177
+ const body = await response.json();
178
+ const resolvedName = isRecord(body) && typeof body.name === 'string' ? body.name : null;
179
+ if (!resolvedName) {
180
+ throw new Error('npm registry response did not include a package name');
181
+ }
182
+ return {
183
+ packageName,
184
+ registryUrl,
185
+ exists: true,
186
+ resolvedName,
187
+ };
188
+ }
153
189
  export async function checkNpmLatestVersion(metadata) {
154
190
  const registryUrl = getRegistryUrl();
155
191
  const response = await fetch(buildLatestPackageUrl(registryUrl, metadata.name), {
@@ -198,6 +198,15 @@ const PUBLIC_JSON_SCHEMA_CONTRACTS = [
198
198
  installedCommand: ['mf', 'line-endings', 'check', '--json'],
199
199
  expectedExitCodes: [0, 1],
200
200
  },
201
+ {
202
+ id: 'quality-gaming-report',
203
+ schemaFile: 'quality-gaming-report.schema.json',
204
+ producer: 'mf quality check --json',
205
+ packaged: true,
206
+ documented: true,
207
+ installedCommand: ['mf', 'quality', 'check', '--json'],
208
+ expectedExitCodes: [0, 1],
209
+ },
201
210
  {
202
211
  id: 'latest-run-pointer',
203
212
  schemaFile: 'latest-run-pointer.schema.json',
@@ -0,0 +1,300 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { createCommandEnv } from './command-env.js';
5
+ import { readFileInsideWithoutSymlinks } from './safe-filesystem.js';
6
+ const LONG_LINE_THRESHOLD = 160;
7
+ const MULTI_STATEMENT_SEMICOLON_THRESHOLD = 1;
8
+ const LARGE_HELPER_LINE_THRESHOLD = 120;
9
+ const TEXT_FILE_PATTERN = /\.(?:cjs|cts|css|go|h|hpp|html|java|js|jsx|json|kt|md|mjs|mts|php|ps1|py|rb|rs|scss|sh|sql|svelte|swift|toml|ts|tsx|txt|vue|xml|yaml|yml)$/iu;
10
+ const CODE_FILE_PATTERN = /\.(?:cjs|cts|go|java|js|jsx|kt|mjs|mts|php|ps1|py|rb|rs|svelte|swift|ts|tsx|vue)$/iu;
11
+ const GENERATED_OR_VENDOR_PATTERN = /(?:^|\/)(?:generated|vendor|third_party)(?:\/|$)/iu;
12
+ const HELPER_CONTAINER_PATTERN = /(?:^|\/)[^/]*(?:helper|helpers|util|utils|manager|common|misc)[^/]*\.(?:cjs|cts|go|java|js|jsx|kt|mjs|mts|php|ps1|py|rb|rs|svelte|swift|ts|tsx|vue)$/iu;
13
+ const SUPPRESSION_PATTERN = /(?:eslint-disable|ts-ignore|ts-expect-error|biome-ignore|rome-ignore|noqa|type:\s*ignore|pragma:\s*no cover|istanbul\s+ignore|c8\s+ignore)/iu;
14
+ const TYPE_ESCAPE_PATTERN = /\b(?:as\s+any|as\s+unknown\s+as|as\s+never|:\s*any\b|!\.|!\)|!\]|!\s*[,;]|\bObject\s+as\s+unknown)\b/u;
15
+ const TEST_BYPASS_PATTERN = /(?:\.skip\b|\.only\b|@Disabled\b|@Ignore\b|\bxfail\b|pytest\.mark\.skip|it\.todo\b|test\.todo\b)/u;
16
+ const PLACEHOLDER_PATTERN = /(?:TODO|FIXME|HACK|not\s+implemented|unimplemented|throw\s+new\s+Error\(["'`](?:not implemented|todo|unsupported)|\bpass\s*$)/iu;
17
+ const THROW_PLACEHOLDER_MESSAGE_PATTERN = /(?:not\s+implemented|unimplemented|todo|unsupported)/iu;
18
+ function toPosixPath(value) {
19
+ return value.split(path.sep).join('/');
20
+ }
21
+ function normalizeGitPath(value) {
22
+ return value.replace(/\\/gu, '/');
23
+ }
24
+ function gitList(projectRoot, args) {
25
+ const result = spawnSync('git', [...args, '-z'], {
26
+ cwd: projectRoot,
27
+ encoding: 'buffer',
28
+ env: createCommandEnv(projectRoot, { policy: 'minimal', allowlist: [] }),
29
+ stdio: ['ignore', 'pipe', 'pipe'],
30
+ timeout: 30_000,
31
+ windowsHide: true,
32
+ });
33
+ if (result.status !== 0) {
34
+ return null;
35
+ }
36
+ return result.stdout
37
+ .toString('utf8')
38
+ .split('\0')
39
+ .map((entry) => normalizeGitPath(entry.trim()))
40
+ .filter((entry) => entry.length > 0)
41
+ .sort((left, right) => left.localeCompare(right));
42
+ }
43
+ function gitText(projectRoot, args) {
44
+ const result = spawnSync('git', args, {
45
+ cwd: projectRoot,
46
+ encoding: 'utf8',
47
+ env: createCommandEnv(projectRoot, { policy: 'minimal', allowlist: [] }),
48
+ stdio: ['ignore', 'pipe', 'pipe'],
49
+ timeout: 30_000,
50
+ windowsHide: true,
51
+ });
52
+ return result.status === 0 ? result.stdout : null;
53
+ }
54
+ function listGitTrackedFiles(projectRoot) {
55
+ return gitList(projectRoot, ['ls-files']);
56
+ }
57
+ function listGitChangedFiles(projectRoot) {
58
+ const changedLists = [
59
+ gitList(projectRoot, ['diff', '--name-only', '--diff-filter=ACMR']),
60
+ gitList(projectRoot, ['diff', '--cached', '--name-only', '--diff-filter=ACMR']),
61
+ gitList(projectRoot, ['ls-files', '--others', '--exclude-standard']),
62
+ ];
63
+ if (changedLists.some((list) => list === null)) {
64
+ return null;
65
+ }
66
+ return [...new Set(changedLists.flatMap((list) => list ?? []))].sort((left, right) => left.localeCompare(right));
67
+ }
68
+ function isTextCandidate(relativePath) {
69
+ return TEXT_FILE_PATTERN.test(relativePath) && !relativePath.startsWith('.mustflow/state/');
70
+ }
71
+ function isCodeCandidate(relativePath) {
72
+ return CODE_FILE_PATTERN.test(relativePath);
73
+ }
74
+ function isMeaningfulCodeLine(text) {
75
+ const trimmed = text.trim();
76
+ return trimmed.length > 0 && !trimmed.startsWith('//') && !trimmed.startsWith('#') && !trimmed.startsWith('*');
77
+ }
78
+ function stripQuotedText(text) {
79
+ let stripped = '';
80
+ let quote = null;
81
+ let escaped = false;
82
+ for (const char of text) {
83
+ if (quote) {
84
+ stripped += ' ';
85
+ if (escaped) {
86
+ escaped = false;
87
+ continue;
88
+ }
89
+ if (char === '\\') {
90
+ escaped = true;
91
+ continue;
92
+ }
93
+ if (char === quote) {
94
+ quote = null;
95
+ }
96
+ continue;
97
+ }
98
+ if (char === '"' || char === "'" || char === '`') {
99
+ quote = char;
100
+ stripped += ' ';
101
+ continue;
102
+ }
103
+ stripped += char;
104
+ }
105
+ return stripped;
106
+ }
107
+ function stripRegexLiterals(text) {
108
+ const patternDeclaration = /^(\s*(?:const\s+[A-Z0-9_]+\s*=\s*)?)\/.*\/[a-z]*;?\s*$/u;
109
+ if (patternDeclaration.test(text)) {
110
+ return text.replace(/\/.*\/[a-z]*;?/u, 'REGEX;');
111
+ }
112
+ return text.replace(/([=(:,\[]\s*)\/.*\/[a-z]*/giu, '$1 REGEX');
113
+ }
114
+ function codeSurfaceForLine(text) {
115
+ const literalStripped = stripQuotedText(stripRegexLiterals(text));
116
+ return literalStripped.split('//', 1)[0] ?? literalStripped;
117
+ }
118
+ function isCommentOnlyLine(text) {
119
+ const trimmed = text.trim();
120
+ return trimmed.startsWith('//') || trimmed.startsWith('#') || trimmed.startsWith('*');
121
+ }
122
+ function countSemicolonsOutsideComment(text) {
123
+ return [...codeSurfaceForLine(text)].filter((char) => char === ';').length;
124
+ }
125
+ function readTextLines(projectRoot, relativePath, issues) {
126
+ const absolutePath = path.join(projectRoot, ...relativePath.split('/'));
127
+ if (!existsSync(absolutePath)) {
128
+ return [];
129
+ }
130
+ try {
131
+ const buffer = readFileInsideWithoutSymlinks(projectRoot, absolutePath);
132
+ if (buffer.includes(0)) {
133
+ return [];
134
+ }
135
+ return buffer
136
+ .toString('utf8')
137
+ .split(/\r?\n/u)
138
+ .map((text, index) => ({ line: index + 1, text }));
139
+ }
140
+ catch (error) {
141
+ issues.push(error instanceof Error ? error.message : String(error));
142
+ return [];
143
+ }
144
+ }
145
+ function isGitTracked(projectRoot, relativePath) {
146
+ const result = spawnSync('git', ['ls-files', '--error-unmatch', '--', relativePath], {
147
+ cwd: projectRoot,
148
+ encoding: 'utf8',
149
+ env: createCommandEnv(projectRoot, { policy: 'minimal', allowlist: [] }),
150
+ stdio: ['ignore', 'pipe', 'pipe'],
151
+ timeout: 30_000,
152
+ windowsHide: true,
153
+ });
154
+ return result.status === 0;
155
+ }
156
+ function parseAddedLinesFromDiff(diffText) {
157
+ const addedLines = [];
158
+ let nextLine = 0;
159
+ for (const rawLine of diffText.split(/\r?\n/u)) {
160
+ const hunk = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/u.exec(rawLine);
161
+ if (hunk) {
162
+ nextLine = Number.parseInt(hunk[1], 10);
163
+ continue;
164
+ }
165
+ if (nextLine === 0) {
166
+ continue;
167
+ }
168
+ if (rawLine.startsWith('+++')) {
169
+ continue;
170
+ }
171
+ if (rawLine.startsWith('+')) {
172
+ addedLines.push({ line: nextLine, text: rawLine.slice(1) });
173
+ nextLine += 1;
174
+ continue;
175
+ }
176
+ if (rawLine.startsWith('-')) {
177
+ continue;
178
+ }
179
+ nextLine += 1;
180
+ }
181
+ return addedLines;
182
+ }
183
+ function readChangedLineCandidates(projectRoot, relativePath, issues) {
184
+ if (!isGitTracked(projectRoot, relativePath)) {
185
+ return readTextLines(projectRoot, relativePath, issues);
186
+ }
187
+ const diffText = gitText(projectRoot, ['diff', 'HEAD', '--no-ext-diff', '--unified=0', '--', relativePath]);
188
+ if (diffText === null) {
189
+ return readTextLines(projectRoot, relativePath, issues);
190
+ }
191
+ return parseAddedLinesFromDiff(diffText);
192
+ }
193
+ function addRisk(risks, code, severity, relativePath, line, detail, metric) {
194
+ risks.push({
195
+ code,
196
+ severity,
197
+ path: toPosixPath(relativePath),
198
+ line,
199
+ detail,
200
+ metric,
201
+ });
202
+ }
203
+ function inspectLine(relativePath, candidate, risks) {
204
+ const text = candidate.text;
205
+ const trimmed = text.trim();
206
+ const isCode = isCodeCandidate(relativePath);
207
+ const codeSurface = codeSurfaceForLine(text);
208
+ const markerSurface = isCommentOnlyLine(text) ? trimmed : codeSurface;
209
+ if (isCode && codeSurface.length > LONG_LINE_THRESHOLD) {
210
+ addRisk(risks, 'line_stuffing_added', 'high', relativePath, candidate.line, 'Added line exceeds the anti-stuffing length threshold.', {
211
+ name: 'line_length',
212
+ value: codeSurface.length,
213
+ threshold: LONG_LINE_THRESHOLD,
214
+ });
215
+ }
216
+ if (isCode &&
217
+ isMeaningfulCodeLine(text) &&
218
+ countSemicolonsOutsideComment(text) > MULTI_STATEMENT_SEMICOLON_THRESHOLD) {
219
+ addRisk(risks, 'multiple_statements_per_line_added', 'high', relativePath, candidate.line, 'Added code line appears to contain multiple statements.', {
220
+ name: 'semicolons',
221
+ value: countSemicolonsOutsideComment(text),
222
+ threshold: MULTI_STATEMENT_SEMICOLON_THRESHOLD,
223
+ });
224
+ }
225
+ if (isCode && SUPPRESSION_PATTERN.test(markerSurface)) {
226
+ addRisk(risks, 'suppression_added', 'critical', relativePath, candidate.line, 'Added line contains a validation suppression marker.', null);
227
+ }
228
+ if (isCode && TYPE_ESCAPE_PATTERN.test(codeSurface)) {
229
+ addRisk(risks, 'type_escape_added', 'high', relativePath, candidate.line, 'Added line contains a broad type escape or non-null assertion pattern.', null);
230
+ }
231
+ if (isCode && TEST_BYPASS_PATTERN.test(codeSurface)) {
232
+ addRisk(risks, 'test_bypass_marker_added', 'critical', relativePath, candidate.line, 'Added line contains a test bypass marker.', null);
233
+ }
234
+ if (isCode &&
235
+ (PLACEHOLDER_PATTERN.test(markerSurface) ||
236
+ (/\bthrow\s+new\s+Error\s*\(/u.test(codeSurface) && THROW_PLACEHOLDER_MESSAGE_PATTERN.test(text)))) {
237
+ addRisk(risks, 'placeholder_added', 'medium', relativePath, candidate.line, 'Added line looks like a placeholder or unfinished implementation.', null);
238
+ }
239
+ if (GENERATED_OR_VENDOR_PATTERN.test(relativePath) && isCode && isMeaningfulCodeLine(text)) {
240
+ addRisk(risks, 'generated_or_vendor_logic_added', 'high', relativePath, candidate.line, 'Added executable-looking logic under generated, vendor, or third_party paths.', null);
241
+ }
242
+ }
243
+ function inspectFile(projectRoot, relativePath, scope, issues) {
244
+ const candidates = scope === 'tracked'
245
+ ? readTextLines(projectRoot, relativePath, issues)
246
+ : readChangedLineCandidates(projectRoot, relativePath, issues);
247
+ const risks = [];
248
+ for (const candidate of candidates) {
249
+ inspectLine(relativePath, candidate, risks);
250
+ }
251
+ if (scope === 'tracked' && HELPER_CONTAINER_PATTERN.test(relativePath) && candidates.length > LARGE_HELPER_LINE_THRESHOLD) {
252
+ addRisk(risks, 'large_helper_container_added', 'medium', relativePath, null, 'Helper, util, manager, common, or misc container exceeds the size threshold.', {
253
+ name: 'file_lines',
254
+ value: candidates.length,
255
+ threshold: LARGE_HELPER_LINE_THRESHOLD,
256
+ });
257
+ }
258
+ if (risks.length === 0) {
259
+ return null;
260
+ }
261
+ return {
262
+ path: toPosixPath(relativePath),
263
+ checked_lines: candidates.length,
264
+ risk_count: risks.length,
265
+ risks,
266
+ };
267
+ }
268
+ export function inspectQualityGaming(projectRoot, options = {}) {
269
+ const root = path.resolve(projectRoot);
270
+ const scope = options.scope ?? 'changed';
271
+ const candidateFiles = scope === 'tracked' ? listGitTrackedFiles(root) : listGitChangedFiles(root);
272
+ const issues = [];
273
+ const riskyFiles = [];
274
+ if (!candidateFiles) {
275
+ issues.push('Git files could not be listed. Run this command inside a Git working tree.');
276
+ }
277
+ for (const relativePath of candidateFiles ?? []) {
278
+ if (!isTextCandidate(relativePath)) {
279
+ continue;
280
+ }
281
+ const fileReport = inspectFile(root, relativePath, scope, issues);
282
+ if (fileReport) {
283
+ riskyFiles.push(fileReport);
284
+ }
285
+ }
286
+ const riskCount = riskyFiles.reduce((sum, file) => sum + file.risk_count, 0);
287
+ return {
288
+ schema_version: '1',
289
+ command: 'quality',
290
+ mode: 'check',
291
+ scope,
292
+ ok: issues.length === 0 && riskCount === 0,
293
+ mustflow_root: root,
294
+ git_tracked: candidateFiles !== null,
295
+ checked_files: candidateFiles?.filter(isTextCandidate).length ?? 0,
296
+ risk_count: riskCount,
297
+ risky_files: riskyFiles,
298
+ issues,
299
+ };
300
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mustflow",
3
- "version": "2.58.0",
3
+ "version": "2.59.0",
4
4
  "description": "Agent workflow documents and CLI for mustflow repository roots.",
5
5
  "type": "module",
6
6
  "license": "MIT-0",
package/schemas/README.md CHANGED
@@ -49,6 +49,9 @@ Current schemas:
49
49
  `mf impact <path...> --json`
50
50
  - `line-endings-report.schema.json`: output of `mf line-endings check --json` and
51
51
  `mf line-endings normalize --json`
52
+ - `quality-gaming-report.schema.json`: output of `mf quality check --json`, containing changed-file
53
+ quality-gaming risks such as line stuffing, validation suppressions, test bypass markers, type
54
+ escapes, generated/vendor logic, and placeholder implementations
52
55
  - `latest-run-pointer.schema.json`: `.mustflow/state/runs/latest.json` when `mf verify` writes a
53
56
  pointer to the latest verify run bundle, including the verify completion verdict, evidence model,
54
57
  and coverage matrix
@@ -0,0 +1,95 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://mustflow.github.io/schemas/quality-gaming-report.schema.json",
4
+ "title": "mustflow quality-gaming report",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": [
8
+ "schema_version",
9
+ "command",
10
+ "mode",
11
+ "scope",
12
+ "ok",
13
+ "mustflow_root",
14
+ "git_tracked",
15
+ "checked_files",
16
+ "risk_count",
17
+ "risky_files",
18
+ "issues"
19
+ ],
20
+ "properties": {
21
+ "schema_version": { "const": "1" },
22
+ "command": { "const": "quality" },
23
+ "mode": { "const": "check" },
24
+ "scope": { "enum": ["changed", "tracked"] },
25
+ "ok": { "type": "boolean" },
26
+ "mustflow_root": { "type": "string" },
27
+ "git_tracked": { "type": "boolean" },
28
+ "checked_files": { "type": "integer", "minimum": 0 },
29
+ "risk_count": { "type": "integer", "minimum": 0 },
30
+ "risky_files": {
31
+ "type": "array",
32
+ "items": { "$ref": "#/$defs/riskyFile" }
33
+ },
34
+ "issues": {
35
+ "type": "array",
36
+ "items": { "type": "string" }
37
+ }
38
+ },
39
+ "$defs": {
40
+ "metric": {
41
+ "type": "object",
42
+ "additionalProperties": false,
43
+ "required": ["name", "value", "threshold"],
44
+ "properties": {
45
+ "name": { "type": "string" },
46
+ "value": { "type": "number" },
47
+ "threshold": { "type": "number" }
48
+ }
49
+ },
50
+ "risk": {
51
+ "type": "object",
52
+ "additionalProperties": false,
53
+ "required": ["code", "severity", "path", "line", "detail", "metric"],
54
+ "properties": {
55
+ "code": {
56
+ "enum": [
57
+ "line_stuffing_added",
58
+ "multiple_statements_per_line_added",
59
+ "suppression_added",
60
+ "type_escape_added",
61
+ "test_bypass_marker_added",
62
+ "placeholder_added",
63
+ "generated_or_vendor_logic_added",
64
+ "large_helper_container_added"
65
+ ]
66
+ },
67
+ "severity": { "enum": ["medium", "high", "critical"] },
68
+ "path": { "type": "string" },
69
+ "line": { "type": ["integer", "null"], "minimum": 1 },
70
+ "detail": { "type": "string" },
71
+ "metric": {
72
+ "anyOf": [
73
+ { "$ref": "#/$defs/metric" },
74
+ { "type": "null" }
75
+ ]
76
+ }
77
+ }
78
+ },
79
+ "riskyFile": {
80
+ "type": "object",
81
+ "additionalProperties": false,
82
+ "required": ["path", "checked_lines", "risk_count", "risks"],
83
+ "properties": {
84
+ "path": { "type": "string" },
85
+ "checked_lines": { "type": "integer", "minimum": 0 },
86
+ "risk_count": { "type": "integer", "minimum": 1 },
87
+ "risks": {
88
+ "type": "array",
89
+ "items": { "$ref": "#/$defs/risk" },
90
+ "minItems": 1
91
+ }
92
+ }
93
+ }
94
+ }
95
+ }
@@ -127,6 +127,22 @@ network = false
127
127
  destructive = false
128
128
  required_after = ["line_ending_warning", "formatting_change"]
129
129
 
130
+ [intents.quality_gaming_check]
131
+ status = "configured"
132
+ kind = "mustflow_builtin"
133
+ lifecycle = "oneshot"
134
+ run_policy = "agent_allowed"
135
+ description = "Inspect changed files for AI quality-gaming patterns."
136
+ argv = ["mf", "quality", "check", "--json"]
137
+ cwd = "."
138
+ timeout_seconds = 120
139
+ stdin = "closed"
140
+ success_exit_codes = [0]
141
+ writes = []
142
+ network = false
143
+ destructive = false
144
+ required_after = ["code_change", "behavior_change", "test_change", "public_api_change", "performance_change", "unknown_change"]
145
+
130
146
  [intents.line_endings_normalize]
131
147
  status = "manual_only"
132
148
  description = "Normalize tracked text files to the repository line-ending policy."
@@ -62,7 +62,7 @@ translations = {}
62
62
  [documents."skills.index"]
63
63
  source = "locales/en/.mustflow/skills/INDEX.md"
64
64
  source_locale = "en"
65
- revision = 167
65
+ revision = 168
66
66
  translations = {}
67
67
 
68
68
  [documents."skill.adapter-boundary"]
@@ -119,6 +119,12 @@ source_locale = "en"
119
119
  revision = 2
120
120
  translations = {}
121
121
 
122
+ [documents."skill.quality-gaming-guard"]
123
+ source = "locales/en/.mustflow/skills/quality-gaming-guard/SKILL.md"
124
+ source_locale = "en"
125
+ revision = 1
126
+ translations = {}
127
+
122
128
  [documents."skill.module-boundary-review"]
123
129
  source = "locales/en/.mustflow/skills/module-boundary-review/SKILL.md"
124
130
  source_locale = "en"
@@ -2,7 +2,7 @@
2
2
  mustflow_doc: skills.index
3
3
  locale: en
4
4
  canonical: true
5
- revision: 167
5
+ revision: 168
6
6
  authority: router
7
7
  lifecycle: mustflow-owned
8
8
  ---
@@ -371,6 +371,7 @@ routes. Event routes stay inactive until their event occurs.
371
371
  | An unfamiliar codebase area needs an evidence-based map before planning, implementation, or reporting | `.mustflow/skills/codebase-orientation/SKILL.md` | User request, target area, relevant instructions, and current source, test, schema, template, configuration, or documentation files | Read-only orientation notes and any smallest follow-up edit chosen from inspected evidence | stale documentation, wrong ownership boundary, or invented architecture claim | `changes_status`, `changes_diff_summary`, `mustflow_check` | Scope inspected, entrypoints, flow map, ownership boundaries, verification options, risks, unknowns, and smallest safe next step |
372
372
  | Large-scope code, docs, tests, content, log, data, or refactor work needs cheap-signal candidate selection before reading or editing many files | `.mustflow/skills/heuristic-candidate-selection/SKILL.md` | User goal, target scope, file-role boundaries, cheap signals, exclusions, scoring factors, batch limit, and verification contract | Candidate discovery, scoring, read plan, bounded batch edits, and directly synchronized surfaces | token waste, false positives, generated-file noise, unimportant cleanup, hallucinated source content, oversized diff, or missed high-impact file | `changes_status`, `changes_diff_summary`, `test_related`, `docs_validate_fast`, `test_release`, `mustflow_check` | Selection goal, excluded surfaces, cheap signals, scored candidates, selected batch, skipped/deferred files, verification, and remaining selection risk |
373
373
  | Assistant-authored, AI-generated, vibe-coded, copied, or broad code changes need a hardening pass for symptom-only fixes, pinpoint hardcoding, exact-value branches, same-defect-class sibling inputs or callers, duplicate helpers, duplicate types or shapes, hidden coupling, circular dependencies, re-export discipline, swallowed errors, excessive nesting, god functions or files, edge cases, and behavior-focused tests | `.mustflow/skills/ai-generated-code-hardening/SKILL.md` | User goal, current diff or target files, reported symptom or failing fixture, existing helpers/types/shapes, sibling inputs or callers, import and export paths, public entrypoints or barrels, error-handling conventions, edge/failure cases, tests, and configured command intents | Small hardening fixes, defect-class rule repair, deduplication into existing single source of truth, boundary notes, focused tests, and directly synchronized docs or contracts | symptom-only patch, pinpoint hardcode, exact literal fixture fix, uninspected sibling input, duplicate abstraction, hidden coupling, circular dependency, accidental public API, string-only tests, over-mocking, fallback sprawl, silent error handling, fan-in/fan-out concentration, or broad rewrite | `changes_status`, `changes_diff_summary`, `test_related`, `test_audit`, `lint`, `build`, `docs_validate_fast`, `test_release`, `mustflow_check` | Sources of truth reused or extended, symptom patch and same-class sibling findings, duplicate/coupling/export/error/complexity/test findings, fixes made, deferred enforcement, verification, and remaining hardening risk |
374
+ | Quality metrics, line-count limits, complexity budgets, lint/type/test gates, or assistant-authored changes may be gamed through long-line stuffing, multiple statements per line, new suppressions, test bypass markers, type escapes, placeholder implementations, generated/vendor logic, giant config blobs, dispatch maps, or helper/util/manager/common containers | `.mustflow/skills/quality-gaming-guard/SKILL.md` | User goal, intended quality outcome behind the metric, current diff, quality gates, formatter/lint/type/test rules, generated/vendor policy, helper naming conventions, suppression baseline, and command contract entries | Real responsibility split, removal of gaming patterns, focused tests or quality-gate evidence, bounded `quality_gaming_check` command contract, and directly synchronized docs or templates | cosmetic metric compliance, line stuffing, validation suppression, test bypass, broad type escape, placeholder success, generated/vendor hiding, junk-drawer helper extraction, or legacy baseline confused with new regression | `quality_gaming_check`, `changes_status`, `changes_diff_summary`, `test_related`, `lint`, `build`, `mustflow_check` | Quality goal, gaming patterns inspected, patterns removed or intentionally left, baseline versus new-regression decision, verification, and remaining quality-gaming risk |
374
375
  | Prompts, prompt builders, system or developer messages, RAG prompt assembly, few-shot examples, structured outputs, tool-use instructions, model selection, reasoning-effort settings, eval sets, refusal or fallback handling, prompt versioning, or AI feature completion criteria are created, changed, reviewed, or reported | `.mustflow/skills/prompt-contract-quality-review/SKILL.md` | Prompt contract ledger, input ledger, authority ledger, output schema, tool policy, model/runtime ledger, RAG evidence ledger, eval ledger, changed files, and command contract entries | Prompt builders, prompt templates, schemas, validators, eval fixtures, boundary examples, tool policies, fallback states, completion definitions, docs, tests, and directly synchronized templates | prompt-as-function gap, user input treated as authority, buried RAG evidence, happy-path-only examples, JSON-parse theater, unpinned production model, hidden prompt storage, raw chain-of-thought request, guessed tool parameters, missing failure state, unbounded reasoning/token cost, or vibe-based prompt improvement claim | `changes_status`, `changes_diff_summary`, `lint`, `build`, `test_related`, `test`, `docs_validate_fast`, `test_release`, `mustflow_check` | Prompt contract reviewed, function boundary, authority and source separation, eval and semantic validation status, model/runtime policy, RAG/tool/failure/completion states, verification, and remaining prompt-contract risk |
375
376
  | LLM answers, RAG responses, citations, source grounding, claim extraction, evidence IDs, answerability states, abstain behavior, retrieval thresholds, tool-backed facts, output validators, LLM judges, or hallucination-control metrics are created, changed, reviewed, or reported | `.mustflow/skills/llm-hallucination-control-review/SKILL.md` | Answer contract ledger, evidence ledger, claim ledger, tool ledger, validator ledger, eval ledger, observability ledger, changed files, and command contract entries | Answerability states, abstain states, missing-information states, source-coverage gates, claim maps, evidence-ID requirements, citation validators, retrieval thresholds, chunk metadata, tool-parameter ownership, deterministic calculators, domain validators, eval fixtures, tests, docs, route metadata, and directly synchronized templates | unsupported factual claim, fabricated citation, source ID invention, weak retrieval gate, noisy semantic-only retrieval, chunk context loss, summary-on-summary hallucination, guessed tool parameter, model arithmetic, source-priority conflict, LLM judge overtrust, low-temperature theater, missing abstain path, missing dirty eval, false citation metric gap, or unobservable grounding drift | `changes_status`, `changes_diff_summary`, `lint`, `build`, `test_related`, `test`, `docs_validate_fast`, `test_release`, `mustflow_check` | Hallucination-control surface reviewed, answerability and abstain states, evidence IDs, claim map, citations, source coverage, validators, retrieval thresholds, tool ownership, evals, metrics, verification, and remaining hallucination-control risk |
376
377
  | LLM API calls, prompt assembly, chat history, RAG context, tool schemas, structured output schemas, model routing, reasoning settings, token budgets, provider prompt caching, app-level response caching, retries, batch or flex processing, predicted outputs, image or file inputs, or LLM cost metrics are created, changed, reviewed, or reported | `.mustflow/skills/llm-token-cost-control-review/SKILL.md` | Cost surface ledger, request ledger, cache ledger, context ledger, output ledger, routing ledger, observability ledger, changed files, and command contract entries | Request builders, prompt prefix ordering, canonical serialization, prompt hashes, cache keys, token counters, budget guards, model routers, context trimming, RAG packing, tool and schema payloads, output patch formats, retry repair paths, metrics, logs, tests, docs, route metadata, and directly synchronized templates | prompt-cache prefix drift, volatile field before stable prefix, unmeasured token count, full transcript replay, RAG chunk bloat, oversized tool or JSON schema payload, expensive model default, unbounded reasoning, no visible output after token cap, full-output regeneration, full-context retry replay, app cache key leak, predicted-output cost confusion, image or file token surprise, or per-call cost hiding cost-per-success regression | `changes_status`, `changes_diff_summary`, `lint`, `build`, `test_related`, `test`, `docs_validate_fast`, `test_release`, `mustflow_check` | LLM token-cost surface reviewed, cost unit and measurement source, stable prefix and cache behavior, app cache/history/RAG/tool/schema/input choices, routing/reasoning/output/retry/Batch/Flex/prediction choices, observability, verification, and remaining token-cost risk |
@@ -0,0 +1,163 @@
1
+ ---
2
+ mustflow_doc: skill.quality-gaming-guard
3
+ locale: en
4
+ canonical: true
5
+ revision: 1
6
+ lifecycle: mustflow-owned
7
+ authority: procedure
8
+ name: quality-gaming-guard
9
+ description: Apply this skill when quality metrics, line-count limits, complexity budgets, lint/type/test gates, or agent-authored changes may be gamed instead of satisfying the intended engineering goal.
10
+ metadata:
11
+ mustflow_schema: "1"
12
+ mustflow_kind: procedure
13
+ pack_id: mustflow.core
14
+ skill_id: mustflow.core.quality-gaming-guard
15
+ command_intents:
16
+ - quality_gaming_check
17
+ - changes_status
18
+ - changes_diff_summary
19
+ - test_related
20
+ - lint
21
+ - build
22
+ - mustflow_check
23
+ ---
24
+
25
+ # Quality Gaming Guard
26
+
27
+ <!-- mustflow-section: purpose -->
28
+ ## Purpose
29
+
30
+ Prevent agents from satisfying visible quality metrics through cheaper evasions such as line
31
+ stuffing, validation suppressions, placeholder implementations, test bypass markers, broad type
32
+ escapes, junk-drawer helpers, or generated/vendor logic hiding.
33
+
34
+ This skill treats numeric limits as smoke alarms, not as the whole design objective. The target is
35
+ the underlying engineering contract: responsibility separation, readable diffs, meaningful tests,
36
+ preserved validation, and maintainable ownership.
37
+
38
+ <!-- mustflow-section: use-when -->
39
+ ## Use When
40
+
41
+ - A task mentions line counts, file size, method size, class size, complexity, lint warnings, type
42
+ errors, test coverage, benchmark thresholds, or quality gates.
43
+ - Assistant-authored code could satisfy a visible metric while making maintainability worse.
44
+ - A change adds or touches tests, validators, lint/type suppressions, generated files, vendor files,
45
+ helper/util/manager/common containers, configuration-as-logic, or placeholder behavior.
46
+ - A repository has a configured `quality_gaming_check` intent or the command contract should expose
47
+ one.
48
+
49
+ <!-- mustflow-section: do-not-use-when -->
50
+ ## Do Not Use When
51
+
52
+ - The task is documentation-only and does not define or change a quality gate.
53
+ - The task is a normal code review with no quality metric or agent-gaming risk; use the narrower
54
+ review or language skill.
55
+ - The repository has no observable files, command contract, or diff evidence to inspect.
56
+
57
+ <!-- mustflow-section: required-inputs -->
58
+ ## Required Inputs
59
+
60
+ - User goal and the intended quality outcome behind any numeric metric.
61
+ - Current changed-file list and diff summary.
62
+ - Relevant quality gates, formatter rules, lint/type/test command intents, and command-contract
63
+ entries.
64
+ - Nearby source, tests, generated/vendor policy, helper naming conventions, and existing
65
+ suppression baseline.
66
+ - Any failing command output or previous workaround evidence.
67
+
68
+ <!-- mustflow-section: preconditions -->
69
+ ## Preconditions
70
+
71
+ - The task matches the Use When conditions and does not match the Do Not Use When exclusions.
72
+ - The intended quality goal has been separated from the visible metric.
73
+ - Higher-priority repository instructions and `.mustflow/config/commands.toml` have been checked.
74
+ - Command execution remains governed by configured command intents only.
75
+
76
+ <!-- mustflow-section: allowed-edits -->
77
+ ## Allowed Edits
78
+
79
+ - Refactor code toward real responsibility ownership instead of only reducing line counts.
80
+ - Add or strengthen focused tests that prove the behavior or quality invariant being protected.
81
+ - Remove newly introduced suppressions, broad type escapes, test bypass markers, placeholder
82
+ implementations, or metric-stuffing shapes when they are in scope.
83
+ - Add or update the bounded `quality_gaming_check` command contract only when the repository can run
84
+ it as a one-shot, read-only intent.
85
+ - Do not weaken tests, assertions, type checking, lint rules, benchmarks, schema validation, or
86
+ command gates to make a metric pass.
87
+
88
+ <!-- mustflow-section: procedure -->
89
+ ## Procedure
90
+
91
+ 1. Name the real engineering goal behind the visible metric. Examples: separate domain ownership,
92
+ keep diffs reviewable, preserve validation strength, keep tests meaningful, or make a hot path
93
+ cheaper.
94
+ 2. Inspect the diff for gaming patterns:
95
+ - long-line stuffing after line-count limits;
96
+ - multiple statements on one line;
97
+ - new lint, type, coverage, or formatter suppressions;
98
+ - `.skip`, `.only`, disabled, xfail, or todo test markers;
99
+ - broad `any`, double assertions, or unsafe non-null assertions;
100
+ - placeholder returns, `not implemented`, `pass`, empty fallbacks, or broad catch swallowing;
101
+ - logic moved into generated/vendor files, giant config blobs, regex tables, dispatch maps, or
102
+ helper/util/manager/common containers.
103
+ 3. When a gaming pattern exists, fix the underlying design rather than only deleting the marker.
104
+ Split by domain responsibility, ownership, input/output boundary, policy owner, or dependency
105
+ direction.
106
+ 4. Prefer formatter, lint, type, test, and architecture evidence that checks multiple dimensions
107
+ together. Do not rely on one numeric gate as proof of design quality.
108
+ 5. For legacy code, distinguish existing baseline from new regression. Existing debt can be reported
109
+ while new debt should be removed or explicitly justified.
110
+ 6. If `quality_gaming_check` is configured, run it after the relevant implementation change. Treat a
111
+ nonzero result as a real quality-gaming finding unless the specific file is intentionally
112
+ excluded by current repository policy.
113
+ 7. Run the narrowest configured verification intents that cover the changed code, tests, command
114
+ contract, and public behavior.
115
+
116
+ <!-- mustflow-section: postconditions -->
117
+ ## Postconditions
118
+
119
+ - The visible metric and the underlying engineering goal are both addressed.
120
+ - No new validation suppression, test bypass, type escape, placeholder, long-line stuffing, or
121
+ generated/vendor hiding remains without an explicit risk note.
122
+ - Any helper/util/manager/common extraction has a real domain responsibility or is reported as a
123
+ remaining design risk.
124
+ - Quality-gaming check results and skipped verification are visible in the final report.
125
+
126
+ <!-- mustflow-section: verification -->
127
+ ## Verification
128
+
129
+ Use configured oneshot command intents when available:
130
+
131
+ - `quality_gaming_check`
132
+ - `changes_status`
133
+ - `changes_diff_summary`
134
+ - `test_related`
135
+ - `lint`
136
+ - `build`
137
+ - `mustflow_check`
138
+
139
+ Use broader configured tests only when the changed behavior or gate is cross-cutting and no narrower
140
+ intent covers it.
141
+
142
+ <!-- mustflow-section: failure-handling -->
143
+ ## Failure Handling
144
+
145
+ - If `quality_gaming_check` reports risks, inspect the file and either remove the gaming pattern or
146
+ report why the repository policy intentionally permits it.
147
+ - If the check is missing, report that the repository has no configured quality-gaming guard instead
148
+ of inventing a command.
149
+ - If a metric conflicts with the real engineering goal, report the conflict and prefer the stronger
150
+ maintainability contract over a cosmetic number.
151
+ - If legacy baseline violations are too broad to fix safely, prevent new violations and report the
152
+ ratchet boundary.
153
+
154
+ <!-- mustflow-section: output-format -->
155
+ ## Output Format
156
+
157
+ - Quality goal behind the metric
158
+ - Gaming patterns inspected
159
+ - Gaming patterns removed, prevented, or intentionally left
160
+ - Baseline versus new-regression decision
161
+ - Command intents run
162
+ - Skipped checks and reasons
163
+ - Remaining quality-gaming risk
@@ -156,6 +156,12 @@ route_type = "adjunct"
156
156
  priority = 74
157
157
  applies_to_reasons = ["unknown_change", "code_change", "behavior_change", "test_change", "public_api_change", "performance_change"]
158
158
 
159
+ [routes."quality-gaming-guard"]
160
+ category = "general_code"
161
+ route_type = "adjunct"
162
+ priority = 76
163
+ applies_to_reasons = ["unknown_change", "code_change", "behavior_change", "test_change", "public_api_change", "performance_change"]
164
+
159
165
  [routes."module-boundary-review"]
160
166
  category = "general_code"
161
167
  route_type = "adjunct"
@@ -1,6 +1,6 @@
1
1
  id = "default"
2
2
  name = "default"
3
- version = "2.58.0"
3
+ version = "2.59.0"
4
4
  description = "Minimal workflow for LLM agents to read, edit, and verify their work in a repository."
5
5
  common_root = "common"
6
6
  locales_root = "locales"
@@ -23,6 +23,7 @@ creates = [
23
23
  ".mustflow/skills/behavior-preserving-refactor/SKILL.md",
24
24
  ".mustflow/skills/code-review/SKILL.md",
25
25
  ".mustflow/skills/ai-generated-code-hardening/SKILL.md",
26
+ ".mustflow/skills/quality-gaming-guard/SKILL.md",
26
27
  ".mustflow/skills/module-boundary-review/SKILL.md",
27
28
  ".mustflow/skills/change-blast-radius-review/SKILL.md",
28
29
  ".mustflow/skills/business-rule-leakage-review/SKILL.md",
@@ -211,6 +212,7 @@ minimal = [
211
212
  "behavior-preserving-refactor",
212
213
  "code-review",
213
214
  "ai-generated-code-hardening",
215
+ "quality-gaming-guard",
214
216
  "module-boundary-review",
215
217
  "change-blast-radius-review",
216
218
  "business-rule-leakage-review",
@@ -339,6 +341,7 @@ patterns = [
339
341
  "behavior-preserving-refactor",
340
342
  "code-review",
341
343
  "ai-generated-code-hardening",
344
+ "quality-gaming-guard",
342
345
  "module-boundary-review",
343
346
  "change-blast-radius-review",
344
347
  "business-rule-leakage-review",
@@ -478,6 +481,7 @@ oss = [
478
481
  "behavior-preserving-refactor",
479
482
  "code-review",
480
483
  "ai-generated-code-hardening",
484
+ "quality-gaming-guard",
481
485
  "module-boundary-review",
482
486
  "change-blast-radius-review",
483
487
  "business-rule-leakage-review",
@@ -632,6 +636,7 @@ team = [
632
636
  "behavior-preserving-refactor",
633
637
  "code-review",
634
638
  "ai-generated-code-hardening",
639
+ "quality-gaming-guard",
635
640
  "module-boundary-review",
636
641
  "change-blast-radius-review",
637
642
  "business-rule-leakage-review",
@@ -772,6 +777,7 @@ product = [
772
777
  "behavior-preserving-refactor",
773
778
  "code-review",
774
779
  "ai-generated-code-hardening",
780
+ "quality-gaming-guard",
775
781
  "module-boundary-review",
776
782
  "change-blast-radius-review",
777
783
  "business-rule-leakage-review",
@@ -918,6 +924,7 @@ library = [
918
924
  "behavior-preserving-refactor",
919
925
  "code-review",
920
926
  "ai-generated-code-hardening",
927
+ "quality-gaming-guard",
921
928
  "module-boundary-review",
922
929
  "change-blast-radius-review",
923
930
  "business-rule-leakage-review",