From ec6e0da8224a54dd7273adc2a797f149fd9795c8 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Wed, 20 May 2026 12:51:53 +0800
Subject: [PATCH] chore: mark stable baseline as 7.6.0
---
Research/SmartStart/Scripts/build-ultimate-default-catalog.mjs | 595 +++++++++++++++++++++++++----------------------------------
1 files changed, 252 insertions(+), 343 deletions(-)
diff --git a/Research/SmartStart/Scripts/build-ultimate-default-catalog.mjs b/Research/SmartStart/Scripts/build-ultimate-default-catalog.mjs
index 87a58f3..a90f29b 100644
--- a/Research/SmartStart/Scripts/build-ultimate-default-catalog.mjs
+++ b/Research/SmartStart/Scripts/build-ultimate-default-catalog.mjs
@@ -10,10 +10,7 @@
const outputDir = path.join(researchDir, "UltimateDefaultCatalog");
const sources = {
- master5000: path.join(researchDir, "MacCommonApps_CandidateTop5000_Master.csv"),
- persona: path.join(researchDir, "PersonaTopApps", "MacPersonaTopApps_Unique.csv"),
- game: path.join(researchDir, "game.csv"),
- top1000: path.join(researchDir, "AppDefaultTags_Review.csv"),
+ curatedCatalog: path.join(outputDir, "SmartStart_UltimateDefaultCatalog.csv"),
appleNotes: path.join(projectDir, "Apptag", "AppleDefaultAppNotes.swift"),
localizationDir: path.join(projectDir, "Apptag", "Localization"),
};
@@ -53,98 +50,46 @@
const stableTags = new Set(stableTagOrder);
const noteLimit = 80;
-const sourcePriority = {
- apple: 100,
- persona: 90,
- top1000: 80,
- game: 70,
- master5000: 60,
-};
-
-const tagOverridesByNormalizedName = new Map(Object.entries({
- "android-ndk": ["development", "utilities"],
- "codeql": ["development", "security"],
- "prince": ["writing", "development", "utilities"],
- "qlstephen": ["utilities", "system-enhancement"],
- "kotlin-lsp": ["development"],
- "ghdl": ["development"],
- "android-sdk-platform-tools": ["development", "utilities"],
- "android-sdk-command-line-tools": ["development", "utilities"],
-}));
-
-const appleCatalogMetadata = new Map(Object.entries({
- "com.apple.activitymonitor": { name: "Activity Monitor", tags: ["system", "utilities", "system-enhancement"] },
- "com.apple.airport.airportutility": { name: "AirPort Utility", tags: ["system", "utilities"] },
- "com.apple.appstore": { name: "App Store", tags: ["system", "utilities"] },
- "com.apple.apps.launcher": { name: "Apps", tags: ["system", "utilities"] },
- "com.apple.audio.audiomidisetup": { name: "Audio MIDI Setup", tags: ["audio", "system", "utilities"] },
- "com.apple.automator": { name: "Automator", tags: ["system-enhancement", "utilities"] },
- "com.apple.bluetoothfileexchange": { name: "Bluetooth File Exchange", tags: ["transfer", "system", "utilities"] },
- "com.apple.ibooksx": { name: "Books", tags: ["writing", "education", "entertainment"] },
- "com.apple.bootcampassistant": { name: "Boot Camp Assistant", tags: ["system", "utilities"] },
- "com.apple.calculator": { name: "Calculator", tags: ["utilities"] },
- "com.apple.ical": { name: "Calendar", tags: ["productivity"] },
- "com.apple.chess": { name: "Chess", tags: ["game", "entertainment"] },
- "com.apple.clock": { name: "Clock", tags: ["productivity", "utilities"] },
- "com.apple.colorsyncutility": { name: "ColorSync Utility", tags: ["design", "picture-photo", "system", "utilities"] },
- "com.apple.console": { name: "Console", tags: ["system", "utilities"] },
- "com.apple.addressbook": { name: "Contacts", tags: ["communication", "productivity"] },
- "com.apple.dictionary": { name: "Dictionary", tags: ["writing", "education", "utilities"] },
- "com.apple.digitalcolormeter": { name: "Digital Color Meter", tags: ["design", "picture-photo", "utilities"] },
- "com.apple.diskutility": { name: "Disk Utility", tags: ["system", "utilities"] },
- "com.apple.facetime": { name: "FaceTime", tags: ["communication", "video"] },
- "com.apple.findmy": { name: "Find My", tags: ["system", "utilities"] },
- "com.apple.fontbook": { name: "Font Book", tags: ["design", "utilities"] },
- "com.apple.freeform": { name: "Freeform", tags: ["productivity", "design"] },
- "com.apple.games": { name: "Games", tags: ["game", "entertainment"] },
- "com.apple.grapher": { name: "Grapher", tags: ["education", "utilities"] },
- "com.apple.home": { name: "Home", tags: ["system", "utilities"] },
- "com.apple.image_capture": { name: "Image Capture", tags: ["picture-photo", "transfer", "utilities"] },
- "com.apple.generativeplaygroundapp": { name: "Image Playground", tags: ["ai-tools", "picture-photo", "design"] },
- "com.apple.imovieapp": { name: "iMovie", tags: ["video", "media"] },
- "com.apple.screencontinuity": { name: "iPhone Mirroring", tags: ["system-enhancement", "utilities"] },
- "com.apple.journal": { name: "Journal", tags: ["writing", "productivity"] },
- "com.apple.iwork.keynote": { name: "Keynote", tags: ["productivity", "writing"] },
- "com.apple.magnifier": { name: "Magnifier", tags: ["system", "utilities"] },
- "com.apple.mail": { name: "Mail", tags: ["communication"] },
- "com.apple.maps": { name: "Maps", tags: ["productivity", "utilities"] },
- "com.apple.mobilesms": { name: "Messages", tags: ["communication"] },
- "com.apple.migrateassistant": { name: "Migration Assistant", tags: ["transfer", "system", "utilities"] },
- "com.apple.exposelauncher": { name: "Mission Control", tags: ["system", "system-enhancement"] },
- "com.apple.music": { name: "Music", tags: ["audio", "media", "entertainment"] },
- "com.apple.news": { name: "News", tags: ["media", "writing"] },
- "com.apple.notes": { name: "Notes", tags: ["writing", "productivity"] },
- "com.apple.iwork.numbers": { name: "Numbers", tags: ["productivity"] },
- "com.apple.iwork.pages": { name: "Pages", tags: ["writing"] },
- "com.apple.passwords": { name: "Passwords", tags: ["security", "utilities"] },
- "com.apple.mobilephone": { name: "Phone", tags: ["communication"] },
- "com.apple.photobooth": { name: "Photo Booth", tags: ["picture-photo", "media", "entertainment"] },
- "com.apple.photos": { name: "Photos", tags: ["picture-photo", "media"] },
- "com.apple.podcasts": { name: "Podcasts", tags: ["audio", "media", "entertainment"] },
- "com.apple.preview": { name: "Preview", tags: ["picture-photo", "writing", "utilities"] },
- "com.apple.printcenter": { name: "Print Center", tags: ["system", "utilities"] },
- "com.apple.quicktimeplayerx": { name: "QuickTime Player", tags: ["video", "audio", "media"] },
- "com.apple.reminders": { name: "Reminders", tags: ["productivity"] },
- "com.apple.safari": { name: "Safari", tags: ["browser"] },
- "com.apple.screensharing": { name: "Screen Sharing", tags: ["system", "communication", "utilities"] },
- "com.apple.screenshot.launcher": { name: "Screenshot", tags: ["picture-photo", "video", "utilities"] },
- "com.apple.scripteditor2": { name: "Script Editor", tags: ["development", "system-enhancement", "utilities"] },
- "com.apple.sfsymbols": { name: "SF Symbols", tags: ["design", "development"] },
- "com.apple.shortcuts": { name: "Shortcuts", tags: ["system-enhancement", "productivity", "utilities"] },
- "com.apple.siri.launcher": { name: "Siri", tags: ["system", "utilities"] },
- "com.apple.stickies": { name: "Stickies", tags: ["writing", "productivity"] },
- "com.apple.stocks": { name: "Stocks", tags: ["finance"] },
- "com.apple.systemprofiler": { name: "System Information", tags: ["system", "utilities"] },
- "com.apple.systempreferences": { name: "System Settings", tags: ["system", "utilities"] },
- "com.apple.terminal": { name: "Terminal", tags: ["development", "system", "utilities"] },
- "com.apple.textedit": { name: "TextEdit", tags: ["writing"] },
- "com.apple.backup.launcher": { name: "Time Machine", tags: ["system", "utilities"] },
- "com.apple.helpviewer": { name: "Tips", tags: ["education", "utilities"] },
- "com.apple.tv": { name: "TV", tags: ["video", "media", "entertainment"] },
- "com.apple.voicememos": { name: "Voice Memos", tags: ["audio", "media"] },
- "com.apple.voiceoverutility": { name: "VoiceOver Utility", tags: ["system", "utilities"] },
- "com.apple.weather": { name: "Weather", tags: ["utilities"] },
- "com.apple.dt.xcode": { name: "Xcode", tags: ["development"] },
+const tagerToStableTags = new Map(Object.entries({
+ "3d-cad": ["design"],
+ "Automation": ["productivity", "system-enhancement"],
+ "Font": ["design", "utilities"],
+ "GTD": ["productivity"],
+ "Meeting": ["communication", "productivity"],
+ "Notes": ["productivity", "writing"],
+ "PDF": ["writing", "utilities"],
+ "ai-tools": ["ai-tools"],
+ "api-tools": ["development"],
+ "audio": ["media", "audio"],
+ "browser": ["browser"],
+ "communication": ["communication"],
+ "database-tools": ["development"],
+ "design": ["design"],
+ "device-management": ["utilities", "system-enhancement"],
+ "devops": ["development"],
+ "diagramming": ["design"],
+ "education": ["education"],
+ "entertainment": ["entertainment"],
+ "file-management": ["file-management", "utilities"],
+ "finance": ["finance"],
+ "game": ["entertainment", "game"],
+ "ide": ["development"],
+ "input-tools": ["utilities", "system-enhancement"],
+ "media": ["media"],
+ "network-tools": ["utilities"],
+ "office": ["productivity", "writing"],
+ "picture-photo": ["media", "picture-photo"],
+ "runtime-sdk": ["development"],
+ "security": ["security"],
+ "system": ["system", "utilities"],
+ "system-maintenance": ["utilities", "system-enhancement"],
+ "terminal-tools": ["development"],
+ "transfer": ["transfer", "utilities"],
+ "ui-prototyping": ["design"],
+ "utilities": ["utilities"],
+ "video": ["media", "video"],
+ "window-management": ["utilities", "system-enhancement"],
+ "writing": ["writing"],
}));
function parseCSV(text) {
@@ -258,15 +203,33 @@
return Array.from(String(value ?? "")).length;
}
+function stripTerminalPunctuation(value) {
+ return String(value ?? "")
+ .replace(/\s+/g, " ")
+ .trim()
+ .replace(/[。.\.!!\??;;::,,、]+$/u, "")
+ .trim();
+}
+
+function stripLeadingPunctuation(value) {
+ const clean = String(value ?? "")
+ .replace(/\s+/g, " ")
+ .trim();
+
+ if (clean.startsWith(".NET")) {
+ return `Microsoft .NET${clean.slice(".NET".length)}`.trim();
+ }
+
+ return clean
+ .replace(/^[\p{P}\s]+/u, "")
+ .trim();
+}
+
function truncateNote(value) {
- const clean = String(value ?? "").replace(/\s+/g, " ").trim();
+ const clean = stripTerminalPunctuation(stripLeadingPunctuation(value));
if (charLength(clean) <= noteLimit) return clean;
let sliced = Array.from(clean).slice(0, noteLimit).join("");
- sliced = sliced.replace(/[,、;:,.!!??]$/, "");
- if (!/[。.!!??]$/.test(sliced)) {
- sliced = `${Array.from(sliced).slice(0, noteLimit - 1).join("")}。`;
- }
- return sliced;
+ return stripTerminalPunctuation(sliced);
}
function loadCategoryTranslations() {
@@ -284,34 +247,18 @@
return translations;
}
-function localizedGeneratedNote(name, tags, code, translations) {
- const categoryMap = translations.get(code) ?? translations.get("en") ?? {};
- const meaningfulTags = tags.filter((tag) => tag !== "other").slice(0, 2);
- const shownTags = meaningfulTags.length > 0 ? meaningfulTags : tags.slice(0, 1);
- const labels = shownTags.map((tag) => categoryMap[tag] ?? tag);
- const joiner = ["ar", "ar-Najdi"].includes(code) ? "، " : ["zh-Hans", "zh-Hant", "ja"].includes(code) ? "、" : ", ";
- const separator = ["zh-Hans", "zh-Hant", "ja"].includes(code) ? ":" : ": ";
- const end = ["zh-Hans", "zh-Hant", "ja"].includes(code) ? "。" : ".";
- const full = `${name}${separator}${labels.join(joiner)}${end}`;
- if (charLength(full) <= noteLimit) return full;
-
- const compact = `${name}${separator}${labels[0] ?? "App"}${end}`;
- if (charLength(compact) <= noteLimit) return compact;
-
- return truncateNote(compact);
+function inferSourceNoteLanguage(note) {
+ const value = String(note ?? "");
+ if (/[\u3040-\u30ff]/u.test(value)) return "ja";
+ if (/[\uac00-\ud7af]/u.test(value)) return "ko";
+ return "zh-Hans";
}
-function buildLocalizedNotes(name, tags, noteZH, translations) {
+function buildLocalizedNotes(noteZH) {
if (!noteZH) return {};
- const notes = {};
- for (const code of translations.keys()) {
- if (code === "zh-Hans") {
- notes[code] = truncateNote(noteZH);
- } else {
- notes[code] = localizedGeneratedNote(name, tags, code, translations);
- }
- }
- return notes;
+ return {
+ [inferSourceNoteLanguage(noteZH)]: truncateNote(noteZH),
+ };
}
function parseAppleNotes() {
@@ -391,236 +338,179 @@
stats.mergedRows += 1;
}
+function mapCuratedTags(value) {
+ const tags = [];
+ const unknownTokens = [];
+ for (const token of splitTags(value)) {
+ const mapped = tagerToStableTags.get(token);
+ if (!mapped) {
+ unknownTokens.push(token);
+ continue;
+ }
+ tags.push(...mapped);
+ }
+
+ return {
+ tags: orderedTags(tags.length > 0 ? tags : ["other"]),
+ unknownTokens,
+ };
+}
+
+const duplicateAliasSuffixes = ["-app", "-desktop", "-mac", "-macos"];
+
+function duplicateAliasBase(normalizedName) {
+ const clean = String(normalizedName ?? "").trim();
+ for (const suffix of duplicateAliasSuffixes) {
+ if (clean.endsWith(suffix)) {
+ return clean.slice(0, -suffix.length);
+ }
+ }
+ return clean;
+}
+
+function duplicateComparable(row) {
+ return JSON.stringify({
+ defaultTag: String(row.defaultTag ?? "").trim(),
+ tager: String(row.tager ?? "").trim(),
+ bundleIdentifier: normalizeBundle(row.bundleIdentifier),
+ noteZH: truncateNote(row["defaultNote-ZH"]),
+ });
+}
+
+function dedupeCuratedRows(rows, stats) {
+ const byNormalized = new Map();
+ for (const row of rows) {
+ const normalizedName = String(row.normalizedName ?? "").trim() || normalizeName(row.Name ?? "");
+ if (normalizedName && !byNormalized.has(normalizedName)) {
+ byNormalized.set(normalizedName, row);
+ }
+ }
+
+ const filtered = [];
+ for (const row of rows) {
+ const normalizedName = String(row.normalizedName ?? "").trim() || normalizeName(row.Name ?? "");
+ const aliasBase = duplicateAliasBase(normalizedName);
+ const baseRow = aliasBase === normalizedName ? null : byNormalized.get(aliasBase);
+ if (baseRow && duplicateComparable(row) === duplicateComparable(baseRow)) {
+ stats.aliasDuplicatesRemoved += 1;
+ continue;
+ }
+ filtered.push(row);
+ }
+ return filtered;
+}
+
function build() {
fs.mkdirSync(outputDir, { recursive: true });
const translations = loadCategoryTranslations();
const appleNotes = parseAppleNotes();
- const entries = [];
- const byBundle = new Map();
- const byName = new Map();
- const ambiguous = [];
- const mergedGroups = [];
+ const previousRuntimeEntries = fs.existsSync(outputs.runtimeJSON)
+ ? new Map(
+ (JSON.parse(fs.readFileSync(outputs.runtimeJSON, "utf8")).entries ?? []).map((entry) => [entry.normalizedName, entry]),
+ )
+ : new Map();
+ const rawCuratedRows = readCSVObjects(sources.curatedCatalog);
+ const unknownTagerTokens = new Set();
const stats = {
- sourceRows: {},
- mergedRows: 0,
+ curatedRows: rawCuratedRows.length,
+ aliasDuplicatesRemoved: 0,
+ rowsWithSourceNotes: 0,
appleNotesAttached: 0,
- appleNotesSwiftOnly: 0,
+ tagChangedVsPrevious: 0,
+ zhNoteChangedVsPrevious: 0,
+ bundleChangedVsPrevious: 0,
+ missingLocalizedNotes: 0,
};
+ const curatedRows = dedupeCuratedRows(rawCuratedRows, stats);
+ const finalRows = curatedRows.map((row, index) => {
+ const name = String(row.Name ?? "").trim();
+ const normalizedName = String(row.normalizedName ?? "").trim() || normalizeName(name);
+ const bundleIdentifier = normalizeBundle(row.bundleIdentifier);
+ const legacyTags = orderedTags(splitTags(row.defaultTag));
+ const mapping = mapCuratedTags(row.tager);
+ for (const token of mapping.unknownTokens) unknownTagerTokens.add(token);
- function addEntry(incoming) {
- if (!incoming.name || incoming.tags.length === 0) return;
+ let noteZH = truncateNote(row["defaultNote-ZH"]);
+ let usedAppleFallback = false;
+ if (!noteZH) {
+ const bundleNote = bundleIdentifier ? appleNotes.byBundle.get(bundleIdentifier.toLowerCase()) : null;
+ const nameNote = appleNotes.byName.get(name);
+ if (bundleNote || nameNote) {
+ noteZH = truncateNote(bundleNote ?? nameNote);
+ usedAppleFallback = true;
+ stats.appleNotesAttached += 1;
+ }
+ } else {
+ stats.rowsWithSourceNotes += 1;
+ }
- const bundleKey = incoming.bundleIdentifier?.toLowerCase() ?? null;
- let existing = bundleKey ? byBundle.get(bundleKey) : null;
-
- if (!existing) {
- const normalizedExisting = byName.get(incoming.normalizedName);
- if (normalizedExisting) {
- const existingBundle = normalizedExisting.bundleIdentifier?.toLowerCase() ?? null;
- if (bundleKey && existingBundle && bundleKey !== existingBundle) {
- ambiguous.push({
- kind: "same_normalized_different_bundle",
- identity: incoming.normalizedName,
- names: [normalizedExisting.name, incoming.name].join(" | "),
- bundles: [normalizedExisting.bundleIdentifier, incoming.bundleIdentifier].join(" | "),
- sources: [[...normalizedExisting.sources].join("|"), incoming.source].join(" | "),
- decision: "kept_separate_for_review",
- });
- } else {
- existing = normalizedExisting;
- }
+ const notes = buildLocalizedNotes(noteZH);
+ const previous = previousRuntimeEntries.get(normalizedName);
+ if (previous) {
+ if (JSON.stringify(previous.defaultTag ?? []) !== JSON.stringify(mapping.tags)) {
+ stats.tagChangedVsPrevious += 1;
+ }
+ if (((previous.notes ?? {})["zh-Hans"] ?? "") !== (noteZH ?? "")) {
+ stats.zhNoteChangedVsPrevious += 1;
+ }
+ if ((previous.bundleIdentifier ?? null) !== bundleIdentifier) {
+ stats.bundleChangedVsPrevious += 1;
}
}
- if (!existing) {
- const entry = {
- name: incoming.name,
- normalizedName: incoming.normalizedName,
- bundleIdentifier: incoming.bundleIdentifier,
- tags: incoming.tags,
- noteZH: incoming.noteZH,
- rank: incoming.rank,
- sourceEvidence: new Set([...incoming.evidence, incoming.source].filter(Boolean)),
- sources: new Set([incoming.source]),
- namePriority: incoming.priority,
- notePriority: incoming.noteZH ? incoming.priority : -1,
- };
- entries.push(entry);
- byName.set(entry.normalizedName, entry);
- if (bundleKey) byBundle.set(bundleKey, entry);
- return;
- }
-
- const beforeSources = new Set(existing.sources);
- mergeInto(existing, incoming, stats);
- if (incoming.bundleIdentifier) byBundle.set(incoming.bundleIdentifier.toLowerCase(), existing);
- byName.set(existing.normalizedName, existing);
- mergedGroups.push({
- identity: bundleKey ? `bundle:${bundleKey}` : `name:${incoming.normalizedName}`,
- sourcesBefore: [...beforeSources].join("|"),
- mergedSource: incoming.source,
- finalName: existing.name,
- });
- }
-
- const masterRows = readCSVObjects(sources.master5000);
- stats.sourceRows.master5000 = masterRows.length;
- for (const row of masterRows) {
- addEntry(rawEntry({
- source: "master5000",
- name: row.name,
- normalizedName: row.normalizedName,
- bundleIdentifier: row.bundleIdentifier,
- tags: splitTags(row.defaultTagIDs),
- rank: row.rank,
- evidence: splitTags(row.sourceEvidence),
- }));
- }
-
- const topRows = readCSVObjects(sources.top1000);
- stats.sourceRows.top1000 = topRows.length;
- for (const row of topRows) {
- addEntry(rawEntry({
- source: "top1000",
- name: row.name,
- normalizedName: row.normalizedName,
- bundleIdentifier: row.bundleIdentifier,
- tags: splitTags(row.defaultTagIDs),
- rank: row.rank,
- evidence: splitTags(row.sourceEvidence),
- }));
- }
-
- const gameRows = readCSVObjects(sources.game);
- stats.sourceRows.game = gameRows.length;
- for (const row of gameRows) {
- addEntry(rawEntry({
- source: "game",
- name: row.name,
- normalizedName: normalizeName(row.name),
- bundleIdentifier: row.bundleIdentifier,
- tags: orderedTags([...splitTags(row.defaultTagIDs), "entertainment"]),
- rank: row.rank,
- evidence: ["game_seed"],
- }));
- }
-
- const personaRows = readCSVObjects(sources.persona);
- stats.sourceRows.persona = personaRows.length;
- for (const row of personaRows) {
- addEntry(rawEntry({
- source: "persona",
- name: row.appName,
- normalizedName: row.normalizedName,
- bundleIdentifier: row.bundleIdentifier,
- tags: splitTags(row.defaultTagIDs),
- noteZH: row.defaultNoteZH,
- rank: row.masterRank || row.bestPersonaRank,
- evidence: ["persona_top_apps", ...(row.personaIDs ? [`personas:${row.personaIDs}`] : [])],
- }));
- }
-
- stats.sourceRows.apple = appleNotes.byBundle.size;
- for (const [bundleIdentifier, note] of appleNotes.byBundle.entries()) {
- const metadata = appleCatalogMetadata.get(bundleIdentifier);
- if (!metadata) continue;
- addEntry(rawEntry({
- source: "apple",
- name: metadata.name,
- normalizedName: normalizeName(metadata.name),
+ return {
+ rank: index + 1,
+ name,
+ normalizedName,
bundleIdentifier,
- tags: metadata.tags,
- noteZH: note,
- rank: 50,
- evidence: ["apple_default_notes"],
- }));
- }
-
- for (const entry of entries) {
- const bundleNote = entry.bundleIdentifier
- ? appleNotes.byBundle.get(entry.bundleIdentifier.toLowerCase())
- : null;
- const nameNote = appleNotes.byName.get(entry.name);
- const note = bundleNote ?? nameNote;
- if (note) {
- entry.noteZH = truncateNote(note);
- entry.notePriority = sourcePriority.apple;
- entry.sources.add("apple");
- entry.sourceEvidence.add("apple_default_notes");
- stats.appleNotesAttached += 1;
- }
- }
-
- for (const bundle of appleNotes.byBundle.keys()) {
- if (!byBundle.has(bundle)) stats.appleNotesSwiftOnly += 1;
- }
-
- const finalRows = entries
- .map((entry) => {
- entry.tags = orderedTags(entry.tags);
- if (entry.tags.length === 1 && entry.tags[0] === "other") {
- const overrideTags = tagOverridesByNormalizedName.get(entry.normalizedName);
- if (overrideTags) {
- entry.tags = orderedTags(overrideTags);
- entry.sourceEvidence.add("manual_other_reduction");
- }
- }
- const notes = buildLocalizedNotes(entry.name, entry.tags, entry.noteZH, translations);
- return {
- name: entry.name,
- normalizedName: entry.normalizedName || normalizeName(entry.name),
- bundleIdentifier: entry.bundleIdentifier,
- tags: entry.tags,
- noteZH: entry.noteZH,
- notes,
- rank: entry.rank,
- sourceEvidence: [...entry.sourceEvidence].sort(),
- sources: [...entry.sources].sort(),
- };
- })
- .sort((left, right) => {
- const rankDiff = left.rank - right.rank;
- if (rankDiff !== 0) return rankDiff;
- return left.name.localeCompare(right.name);
- });
+ tags: mapping.tags,
+ legacyTags,
+ noteZH,
+ notes,
+ sourceEvidence: usedAppleFallback
+ ? ["curated_tager_catalog", "apple_default_notes"]
+ : ["curated_tager_catalog"],
+ };
+ });
const invalidTagRows = finalRows.filter((row) => row.tags.some((tag) => !stableTags.has(tag)));
const emptyNormalizedRows = finalRows.filter((row) => !row.normalizedName);
const exactOtherRows = finalRows.filter((row) => row.tags.length === 1 && row.tags[0] === "other");
const mixedOtherRows = finalRows.filter((row) => row.tags.length > 1 && row.tags.includes("other"));
const noteRows = finalRows.filter((row) => row.noteZH);
- const localizedNoteIssues = [];
+ const noteQualityIssues = [];
for (const row of noteRows) {
- for (const code of translations.keys()) {
- const note = row.notes[code];
- if (!note) {
- localizedNoteIssues.push({ name: row.name, code, issue: "missing" });
- } else if (charLength(note) > noteLimit) {
- localizedNoteIssues.push({ name: row.name, code, issue: `over_limit:${charLength(note)}` });
+ for (const [code, note] of Object.entries(row.notes)) {
+ if (charLength(note) > noteLimit) {
+ noteQualityIssues.push({ name: row.name, code, issue: `over_limit:${charLength(note)}` });
+ } else if (stripTerminalPunctuation(note) != note) {
+ noteQualityIssues.push({ name: row.name, code, issue: "trailing_punctuation" });
+ } else if (stripLeadingPunctuation(note) != note) {
+ noteQualityIssues.push({ name: row.name, code, issue: "leading_punctuation" });
}
}
}
- const reviewHeader = ["Name", "normalizedName", "defaultTag", "bundleIdentifier", "defaultNote-ZH"];
- const reviewRows = [
- reviewHeader,
- ...finalRows.map((row) => [
- row.name,
- row.normalizedName,
- row.tags.join("|"),
- row.bundleIdentifier ?? "null",
- row.noteZH ?? "",
+ const cleanedCuratedRows = [
+ ["Name", "normalizedName", "defaultTag", "tager", "bundleIdentifier", "defaultNote-ZH"],
+ ...curatedRows.map((row) => [
+ String(row.Name ?? "").trim(),
+ String(row.normalizedName ?? "").trim() || normalizeName(row.Name ?? ""),
+ String(row.defaultTag ?? "").trim(),
+ String(row.tager ?? "").trim(),
+ normalizeBundle(row.bundleIdentifier) ?? "null",
+ truncateNote(row["defaultNote-ZH"]),
]),
];
- fs.writeFileSync(outputs.reviewCSV, `${stringifyCSV(reviewRows)}\n`);
+ fs.writeFileSync(outputs.reviewCSV, `${stringifyCSV(cleanedCuratedRows)}\n`);
const runtime = {
version: 2,
generatedAt: new Date().toISOString(),
noteLimit,
supportedLanguages: [...translations.keys()].sort(),
- entries: finalRows.map((row, index) => ({
- rank: index + 1,
+ entries: finalRows.map((row) => ({
+ rank: row.rank,
name: row.name,
normalizedName: row.normalizedName,
bundleIdentifier: row.bundleIdentifier,
@@ -632,14 +522,28 @@
fs.writeFileSync(outputs.runtimeJSON, `${JSON.stringify(runtime, null, 2)}\n`);
const duplicateRows = [
- ["kind", "identity", "names", "bundles", "sources", "decision"],
- ...ambiguous.map((row) => [row.kind, row.identity, row.names, row.bundles, row.sources, row.decision]),
+ ["name", "normalizedName", "tager", "legacyDefaultTag", "runtimeDefaultTag", "bundleIdentifier", "defaultNote-ZH"],
+ ...finalRows
+ .filter((row) => row.legacyTags.join("|") !== row.tags.join("|"))
+ .map((row) => [
+ row.name,
+ row.normalizedName,
+ (curatedRows[row.rank - 1]?.tager ?? ""),
+ row.legacyTags.join("|"),
+ row.tags.join("|"),
+ row.bundleIdentifier ?? "null",
+ row.noteZH ?? "",
+ ]),
];
fs.writeFileSync(outputs.duplicateReview, `${stringifyCSV(duplicateRows)}\n`);
- const sourceLines = Object.entries(stats.sourceRows)
- .map(([source, count]) => `- ${source}: ${count}`)
- .join("\n");
+ const sourceLines = [
+ "- curated CSV: `Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.csv`",
+ `- curated rows: ${stats.curatedRows}`,
+ `- curated rows after alias dedupe: ${curatedRows.length}`,
+ `- alias duplicates removed: ${stats.aliasDuplicatesRemoved}`,
+ `- unknown tager tokens: ${unknownTagerTokens.size}`,
+ ].join("\n");
const topTagLines = stableTagOrder
.map((tag) => [tag, finalRows.filter((row) => row.tags.includes(tag)).length])
.filter(([, count]) => count > 0)
@@ -664,15 +568,17 @@
- Final rows: ${finalRows.length}
- Rows with Chinese default notes: ${noteRows.length}
-- Merged source rows: ${stats.mergedRows}
-- Ambiguous duplicate candidates: ${ambiguous.length}
+- Rows with source Chinese notes from curated CSV: ${stats.rowsWithSourceNotes}
+- Rows with Apple note fallback attached: ${stats.appleNotesAttached}
- Invalid tag rows: ${invalidTagRows.length}
- Empty normalizedName rows: ${emptyNormalizedRows.length}
- Exact \`other\` rows: ${exactOtherRows.length}
- Mixed \`other\` rows after cleanup: ${mixedOtherRows.length}
-- Apple notes attached to catalog rows: ${stats.appleNotesAttached}
-- Apple notes still handled by Swift-only fallback: ${stats.appleNotesSwiftOnly}
-- Localized note issues: ${localizedNoteIssues.length}
+- Tag changes vs previous runtime JSON: ${stats.tagChangedVsPrevious}
+- Chinese note changes vs previous runtime JSON: ${stats.zhNoteChangedVsPrevious}
+- Bundle identifier changes vs previous runtime JSON: ${stats.bundleChangedVsPrevious}
+- Unknown tager tokens: ${unknownTagerTokens.size}
+- Note quality issues: ${noteQualityIssues.length}
## Tag Distribution
@@ -680,9 +586,9 @@
## Notes
-- \`other\` is removed whenever a row has at least one meaningful tag.
-- Runtime notes are generated for all supported localization files when a Chinese source note exists.
-- Non-Chinese localized notes currently use localized tag-summary copy generated from existing category translations; high-traffic languages can be manually polished later.
+- Runtime tags are generated from the curated CSV \`tager\` column, not the legacy \`defaultTag\` column.
+- Runtime notes are generated only from real source notes. The generator must not synthesize notes from app names or category labels.
+- Missing translations are intentionally omitted until a real translation pipeline or reviewed translation table provides them.
`;
fs.writeFileSync(outputs.report, report);
@@ -694,25 +600,28 @@
- Supported languages: ${runtime.supportedLanguages.length}
- Rows with source Chinese notes: ${noteRows.length}
-- Required localized notes: ${noteRows.length * runtime.supportedLanguages.length}
-- Issues: ${localizedNoteIssues.length}
+- Source notes: ${noteRows.length}
+- Required machine/reviewed translations: ${noteRows.length * (runtime.supportedLanguages.length - 1)}
+- Generated placeholder translations: 0
+- Note quality issues: ${noteQualityIssues.length}
- Note limit: ${noteLimit}
## Issue Sample
-${localizedNoteIssues.slice(0, 50).map((issue) => `- ${issue.name} / ${issue.code}: ${issue.issue}`).join("\n") || "- None"}
+${noteQualityIssues.slice(0, 50).map((issue) => `- ${issue.name} / ${issue.code}: ${issue.issue}`).join("\n") || "- None"}
`;
fs.writeFileSync(outputs.translationQA, translationReport);
return {
finalRows: finalRows.length,
noteRows: noteRows.length,
- ambiguousDuplicates: ambiguous.length,
+ changedVsPrevious: stats.tagChangedVsPrevious,
invalidTagRows: invalidTagRows.length,
emptyNormalizedRows: emptyNormalizedRows.length,
exactOtherRows: exactOtherRows.length,
mixedOtherRows: mixedOtherRows.length,
- localizedNoteIssues: localizedNoteIssues.length,
+ noteQualityIssues: noteQualityIssues.length,
+ unknownTagerTokens: [...unknownTagerTokens].sort(),
outputDir,
};
}
--
Gitblit v1.9.3