Merge feature/nation-turn-audit

This commit is contained in:
2026-07-25 06:18:23 +00:00
36 changed files with 1346 additions and 283 deletions
+32 -23
View File
@@ -1,6 +1,6 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import ts from 'typescript';
import ts from 'typescript-legacy';
const ROOT_DIR = process.cwd();
const PHP_ROOT = path.join(ROOT_DIR, 'legacy', 'hwe', 'sammo', 'Command');
@@ -25,6 +25,7 @@ Options:
--json Print JSON report.
--show-matches Print matched command keys.
--show-compat Print compatibility-matched pairs.
--check Exit non-zero when a command is missing or mismatched.
--no-compat Disable compatibility alias rules (default: enabled).
--compat-file <path> Compatibility rules JSON file (default: tools/compare-command-constraints.compat.json).
--similarity <0..1> Near-match threshold for non-strict mode (default: 0.6).
@@ -32,6 +33,7 @@ Options:
`;
const args = process.argv.slice(2);
const check = args.includes('--check');
if (args.includes('--help')) {
console.log(HELP_TEXT.trim());
process.exit(0);
@@ -114,7 +116,7 @@ const maskPhpComments = (text) => {
continue;
}
if (ch === '\'' || ch === '"') {
if (ch === "'" || ch === '"') {
quote = ch;
i += 1;
continue;
@@ -205,7 +207,7 @@ const scanToDelimiter = (text, startIndex, delimiter) => {
i += 1;
continue;
}
if (ch === '\'' || ch === '"') {
if (ch === "'" || ch === '"') {
quote = ch;
i += 1;
continue;
@@ -242,7 +244,7 @@ const scanToParenEnd = (text, startIndex) => {
i += 1;
continue;
}
if (ch === '\'' || ch === '"') {
if (ch === "'" || ch === '"') {
quote = ch;
i += 1;
continue;
@@ -280,7 +282,7 @@ const splitTopLevel = (text, delimiter) => {
}
continue;
}
if (ch === '\'' || ch === '"') {
if (ch === "'" || ch === '"') {
quote = ch;
current += ch;
continue;
@@ -389,7 +391,7 @@ const stripWrappingQuotes = (value) => {
return '';
}
if (
(text.startsWith('\'') && text.endsWith('\'')) ||
(text.startsWith("'") && text.endsWith("'")) ||
(text.startsWith('"') && text.endsWith('"')) ||
(text.startsWith('`') && text.endsWith('`'))
) {
@@ -830,8 +832,7 @@ const extractPhpFileConstraints = (file, text) => {
regex.lastIndex = endIndex;
}
const classMatch =
/class\s+([\\\p{L}_][\\\p{L}\p{N}_]*)\s+extends\s+([\\\p{L}_][\\\p{L}\p{N}_]*)/mu.exec(text);
const classMatch = /class\s+([\\\p{L}_][\\\p{L}\p{N}_]*)\s+extends\s+([\\\p{L}_][\\\p{L}\p{N}_]*)/mu.exec(text);
const parentClass = classMatch?.[2]?.split('\\').pop() ?? null;
return {
@@ -902,11 +903,7 @@ const readStringLikeProperty = (objLiteral, keyName) => {
if (!ts.isPropertyAssignment(prop)) {
continue;
}
const key = ts.isIdentifier(prop.name)
? prop.name.text
: ts.isStringLiteral(prop.name)
? prop.name.text
: null;
const key = ts.isIdentifier(prop.name) ? prop.name.text : ts.isStringLiteral(prop.name) ? prop.name.text : null;
if (key !== keyName) {
continue;
}
@@ -1337,7 +1334,12 @@ const extractTsFileConstraints = (file, text) => {
let parentFile = null;
const visit = (node) => {
if (!parentFile && ts.isVariableDeclaration(node) && node.initializer && ts.isCallExpression(node.initializer)) {
if (
!parentFile &&
ts.isVariableDeclaration(node) &&
node.initializer &&
ts.isCallExpression(node.initializer)
) {
const callee = node.initializer.expression;
if (ts.isIdentifier(callee)) {
const moduleText = imports.get(callee.text);
@@ -1373,15 +1375,11 @@ const extractTsFileConstraints = (file, text) => {
const name = node.name.text;
if (name === 'buildConstraints') {
assigned.full = true;
byKind.full.push(
...extractTsMethodConstraints(node, sourceFile, 'ts', 'full', relFile, factorySet)
);
byKind.full.push(...extractTsMethodConstraints(node, sourceFile, 'ts', 'full', relFile, factorySet));
}
if (name === 'buildMinConstraints') {
assigned.min = true;
byKind.min.push(
...extractTsMethodConstraints(node, sourceFile, 'ts', 'min', relFile, factorySet)
);
byKind.min.push(...extractTsMethodConstraints(node, sourceFile, 'ts', 'min', relFile, factorySet));
}
}
ts.forEachChild(node, visit);
@@ -1834,7 +1832,9 @@ const printReport = (report) => {
if (result.near.length > 0) {
console.log(` Near match (${result.near.length}):`);
for (const pair of result.near) {
console.log(` - PHP "${pair.phpName}" ~ TS "${pair.tsName}" (score ${pair.score.toFixed(2)})`);
console.log(
` - PHP "${pair.phpName}" ~ TS "${pair.tsName}" (score ${pair.score.toFixed(2)})`
);
}
}
if (result.argDiffs.length > 0) {
@@ -1904,9 +1904,18 @@ const main = async () => {
})),
};
console.log(JSON.stringify(jsonFriendly, null, 2));
return;
} else {
printReport(report);
}
if (
check &&
(report.totals.mismatch > 0 ||
report.totals.nearMatch > 0 ||
report.totals.missingCommandInTs > 0 ||
report.totals.missingCommandInPhp > 0)
) {
process.exitCode = 1;
}
printReport(report);
};
main().catch((error) => {
+75 -74
View File
@@ -1,6 +1,6 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import ts from 'typescript';
import ts from 'typescript-legacy';
const ROOT_DIR = process.cwd();
const PHP_ROOT = path.join(ROOT_DIR, 'legacy', 'hwe', 'sammo', 'Command');
@@ -24,6 +24,7 @@ Options:
--keep-date Keep <1>...</> date markers in normalized output.
--ignore-file <path> JSON ignore list file (default: tools/compare-command-logs.ignore.json).
--checklist Output a markdown checklist for mismatches.
--check Exit non-zero when a command is missing or mismatched.
--json Output JSON report.
--help Show this help.
`;
@@ -40,6 +41,7 @@ const includeGuards = args.includes('--include-guards');
const includeTarget = args.includes('--include-target');
const countSensitive = args.includes('--count-sensitive');
const checklist = args.includes('--checklist');
const check = args.includes('--check');
const ignoreFileIndex = args.indexOf('--ignore-file');
const ignoreFile = ignoreFileIndex >= 0 ? args[ignoreFileIndex + 1] : DEFAULT_IGNORE_FILE;
@@ -62,6 +64,7 @@ const guardPatterns = [
/병종 정보를 확인할 수 없어/,
/현재 선택할 수 없는 병종입니다/,
/도시 정보가 없어/,
/도달할 방법이 없습니다/,
];
const excludeGuards = DEFAULT_EXCLUDE_GUARDS && !includeGuards;
@@ -127,7 +130,7 @@ const scanToDelimiter = (text, startIndex, delimiter) => {
continue;
}
if (ch === '\'' || ch === '"') {
if (ch === "'" || ch === '"') {
quote = ch;
i += 1;
continue;
@@ -166,7 +169,7 @@ const scanToFirstArgumentEnd = (text, startIndex) => {
continue;
}
if (ch === '\'' || ch === '"') {
if (ch === "'" || ch === '"') {
quote = ch;
i += 1;
continue;
@@ -208,7 +211,7 @@ const scanToParenEnd = (text, startIndex) => {
continue;
}
if (ch === '\'' || ch === '"') {
if (ch === "'" || ch === '"') {
quote = ch;
i += 1;
continue;
@@ -249,7 +252,7 @@ const splitTopLevel = (text, delimiter) => {
continue;
}
if (ch === '\'' || ch === '"') {
if (ch === "'" || ch === '"') {
quote = ch;
current += ch;
continue;
@@ -280,7 +283,7 @@ const splitTopLevel = (text, delimiter) => {
const parsePhpStringLiteral = (segment) => {
const trimmed = segment.trim();
const quote = trimmed[0];
if (quote !== '\'' && quote !== '"') {
if (quote !== "'" && quote !== '"') {
return null;
}
@@ -505,7 +508,8 @@ const extractPhpLogCalls = (text, assignments) => {
}
const parsed = parsePhpExprToTemplate(value, assignments, match.index);
const hasGeneralId = methodMeta.generalMethod && !isPhpActorLoggerExpr(calleeExpr, actorGeneralVars, actorLoggerVars);
const hasGeneralId =
methodMeta.generalMethod && !isPhpActorLoggerExpr(calleeExpr, actorGeneralVars, actorLoggerVars);
results.push({
pos: match.index,
@@ -615,10 +619,7 @@ const extractTsLogs = (filePath, text) => {
if (!ts.isIdentifier(decl.name) || !decl.initializer) {
continue;
}
if (
ts.isStringLiteral(decl.initializer) ||
ts.isNoSubstitutionTemplateLiteral(decl.initializer)
) {
if (ts.isStringLiteral(decl.initializer) || ts.isNoSubstitutionTemplateLiteral(decl.initializer)) {
constants.set(decl.name.text, decl.initializer.text);
}
}
@@ -684,8 +685,7 @@ const extractTsLogs = (filePath, text) => {
return;
}
const nameProp = arg.properties.find(
(prop) =>
ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name) && prop.name.text === 'name'
(prop) => ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name) && prop.name.text === 'name'
);
if (!nameProp || !ts.isPropertyAssignment(nameProp)) {
ts.forEachChild(node, visitFactory);
@@ -780,8 +780,7 @@ const loadIgnoreConfig = async () => {
const compileIgnoreRules = (config) => {
const global = config.Global ?? {};
const normalizeList = (list) => (Array.isArray(list) ? list.map((item) => normalizeTemplate(String(item))) : []);
const compileRegex = (list) =>
Array.isArray(list) ? list.map((item) => new RegExp(String(item))) : [];
const compileRegex = (list) => (Array.isArray(list) ? list.map((item) => new RegExp(String(item))) : []);
return {
globalTemplates: new Set(normalizeList(global.templates)),
@@ -1127,69 +1126,71 @@ const main = async () => {
if (asJson) {
console.log(JSON.stringify(report, null, 2));
return;
}
} else {
console.log(
`Compare command logs (mode: ${mode}, strict: ${strict ? 'on' : 'off'}, keepDate: ${keepDate ? 'on' : 'off'}, excludeGuards: ${excludeGuards ? 'on' : 'off'}, excludeTarget: ${excludeTarget ? 'on' : 'off'}, countSensitive: ${countSensitive ? 'on' : 'off'})`
);
console.log(`PHP commands: ${report.totals.phpCommands}`);
console.log(`TS commands: ${report.totals.tsCommands}`);
console.log(`Matched commands: ${report.totals.matches}`);
console.log(`Mismatched commands: ${report.totals.mismatches}`);
console.log(`Missing in TS: ${report.missingInTs.length}`);
console.log(`Missing in PHP: ${report.missingInPhp.length}`);
console.log(`Ignored mismatches: ${report.totals.ignored}`);
console.log(
`Compare command logs (mode: ${mode}, strict: ${strict ? 'on' : 'off'}, keepDate: ${keepDate ? 'on' : 'off'}, excludeGuards: ${excludeGuards ? 'on' : 'off'}, excludeTarget: ${excludeTarget ? 'on' : 'off'}, countSensitive: ${countSensitive ? 'on' : 'off'})`
);
console.log(`PHP commands: ${report.totals.phpCommands}`);
console.log(`TS commands: ${report.totals.tsCommands}`);
console.log(`Matched commands: ${report.totals.matches}`);
console.log(`Mismatched commands: ${report.totals.mismatches}`);
console.log(`Missing in TS: ${report.missingInTs.length}`);
console.log(`Missing in PHP: ${report.missingInPhp.length}`);
console.log(`Ignored mismatches: ${report.totals.ignored}`);
if (report.missingInTs.length > 0) {
console.log('\nMissing in TS:');
for (const key of report.missingInTs) {
console.log(`- ${key}`);
}
}
if (report.missingInPhp.length > 0) {
console.log('\nMissing in PHP:');
for (const key of report.missingInPhp) {
console.log(`- ${key}`);
}
}
if (report.mismatches.length > 0) {
console.log('\nMismatch Details:');
for (const mismatch of report.mismatches) {
console.log(`\n== ${mismatch.key} ==`);
if (mismatch.missingDetails.length > 0) {
console.log(
`PHP only: ${mismatch.missingDetails
.map((item) => (item.count > 1 ? `${item.template} x${item.count}` : item.template))
.join(' | ')}`
);
}
if (mismatch.extraDetails.length > 0) {
console.log(
`TS only: ${mismatch.extraDetails
.map((item) => (item.count > 1 ? `${item.template} x${item.count}` : item.template))
.join(' | ')}`
);
}
const phpLines = formatEntries(mismatch.phpEntries);
const tsLines = formatEntries(mismatch.tsEntries);
console.log('PHP:');
for (const line of phpLines) {
console.log(line);
}
console.log('TS:');
for (const line of tsLines) {
console.log(line);
if (report.missingInTs.length > 0) {
console.log('\nMissing in TS:');
for (const key of report.missingInTs) {
console.log(`- ${key}`);
}
}
}
if (checklist) {
console.log('\nChecklist:');
console.log(renderChecklist(report));
if (report.missingInPhp.length > 0) {
console.log('\nMissing in PHP:');
for (const key of report.missingInPhp) {
console.log(`- ${key}`);
}
}
if (report.mismatches.length > 0) {
console.log('\nMismatch Details:');
for (const mismatch of report.mismatches) {
console.log(`\n== ${mismatch.key} ==`);
if (mismatch.missingDetails.length > 0) {
console.log(
`PHP only: ${mismatch.missingDetails
.map((item) => (item.count > 1 ? `${item.template} x${item.count}` : item.template))
.join(' | ')}`
);
}
if (mismatch.extraDetails.length > 0) {
console.log(
`TS only: ${mismatch.extraDetails
.map((item) => (item.count > 1 ? `${item.template} x${item.count}` : item.template))
.join(' | ')}`
);
}
const phpLines = formatEntries(mismatch.phpEntries);
const tsLines = formatEntries(mismatch.tsEntries);
console.log('PHP:');
for (const line of phpLines) {
console.log(line);
}
console.log('TS:');
for (const line of tsLines) {
console.log(line);
}
}
}
if (checklist) {
console.log('\nChecklist:');
console.log(renderChecklist(report));
}
}
if (check && (report.totals.mismatches > 0 || report.missingInTs.length > 0 || report.missingInPhp.length > 0)) {
process.exitCode = 1;
}
};