Files
deepseek-harness/examples/desktop/tools/comment-sweep-apply.js
ZiyaZhang e8f5c0b51b feat(desktop): DSH Electron desktop shell — harness internals visualized
Minimal Electron shell over the DSH JSON-RPC runtime — a first-look at
what a ChatGPT.app-style host on top of the DeepSeek Harness looks
like, with the harness's normally-invisible internals (trace timeline,
context surface, subagent tree, compaction, plugin registry, rubrics)
brought forward as first-class UI surfaces so plugin authors and
researchers can see what the agent is actually doing.

Runs against three keyless-to-live profiles (stdio-echo works on
master out of the box; daemon-echo / daemon-vibe-echo activate once
the daemon-demo lands; stdio-deepseek and daemon-vibe hit the real
DeepSeek API when you supply a key). HARNESS_DEV auto-resolves to the
in-repo runtime when this shell ships under examples/desktop/, so a
fresh clone launches without config; env DSH_DEV_ROOT overrides for
custom layouts, and a sibling deepseek-harness-dev/ checkout is the
original dev workflow.

Cold-clone gate (P0 fixes for first-time-clone usability):
- HARNESS_DEV: 3-candidate resolver (env → walk-up in-repo marker →
  sibling), unit-tested via mock fs so ordering is locked without
  needing either real layout on disk.
- config yml leaves rewritten at assemble time so the sibling-clone
  paths (../../deepseek-harness-dev/examples/echo-agent/…) become
  the in-repo paths (../../echo-agent/…) in the released tree —
  source yml stays usable for local dev, released tree ships a
  working shape.
- pnpm-workspace.yaml allowBuilds.electron = true (was placeholder).
- missing-key card in stdio-deepseek offers a one-click switch to
  stdio-echo (the keyless profile that works on master) rather than
  daemon-echo (blocked on the not-yet-shipped daemon-demo).
- assemble-oss-release.sh rewrites the source-side breadcrumb name
  'dsh-desktop-demo' → 'dsh-desktop' for the released package.json.

FOUC guard on the onboarding gate (41fc5df carried) keeps the
first-launch splash from flashing before the runtime probe finishes.

Test suite (1634 tests in source, 3990 in the runtime repo) covers
resolver ordering, renderer classifiers, trace timeline shape,
compaction diff rendering, rubric parity, and the missing-key
onboarding paths.
2026-07-18 12:59:34 -07:00

468 lines
22 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/*
* comment-sweep-apply.js
*
* Mechanical stripper. Reads every .js file under src/main, src/preload,
* src/renderer and applies the artifact-reference strip recipe from
* tools/comment-sweep-rules.md.
*
* The stripping is conservative: it edits only comment TEXT (the run of
* characters after `//` or inside `/* … *\/`), not code. If a comment line
* becomes empty after stripping we delete the line; otherwise we rewrite
* the comment in-place, preserving the leading `//` or ` * ` / `/*` / `*\/`
* shape.
*
* Usage:
* node tools/comment-sweep-apply.js --dry-run # print planned edits, don't write
* node tools/comment-sweep-apply.js --apply # write files in place
* node tools/comment-sweep-apply.js --scope=main # main+preload only
* node tools/comment-sweep-apply.js --scope=renderer # renderer only
*
* Scope split maps 1:1 onto the two commits (main+preload / renderer) the
* team-lead brief calls for.
*/
'use strict';
const fs = require('fs');
const path = require('path');
const ROOT = path.resolve(__dirname, '..');
const SCOPES = {
main: ['src/main', 'src/preload'],
renderer: ['src/renderer'],
all: ['src/main', 'src/preload', 'src/renderer'],
};
// -----------------------------------------------------------------------------
// Strip recipe. Each rule is (pattern, replacement, description). Applied in
// order; a rule may match zero, one, or multiple times per line. Rules 1-4
// (LEADING) run only against the START of the comment text.
// -----------------------------------------------------------------------------
// LEADING artifact-prefix rules. Each captures the prefix + trailing
// delimiter (`:` / `—` / `.` / `,`). Applied only if the match starts at
// column 0 of the comment text.
const LEADING_STRIPPERS = [
// Fresh-eyes P0 (2026-07-18, review-fresh-eyes.md #4 [+ team-lead follow-up]):
{ name: 'fresh-eyes-p0-full', re: /^\s*Fresh[- ]eyes\s+P0\s*\([^)]*review[- ]fresh[- ]eyes\.md[^)]*\)\s*[::—]?\s*/i },
// Fresh-eyes P0 (2026-07-18): — dated but no review-md coord
{ name: 'fresh-eyes-p0-dated', re: /^\s*Fresh[- ]eyes\s+P0\s*\([^)]*\)\s*[::—]\s*/i },
// Fresh-eyes P0: (bare)
{ name: 'fresh-eyes-p0-bare', re: /^\s*Fresh[- ]eyes\s+P0\s*[::—]\s*/i },
// Ticket #NNN [X] [(2026-07-17)] [phase 2 (pi §2.3)]: prefix (matches ticket-letter and ticket-num)
{ name: 'ticket-num-prefix', re: /^\s*Ticket\s+#?\d+(?:\s+[A-Z])?(?:\s*\([^)]*\))?(?:\s+step\s+\d+|\s+phase\s+\d+(?:\s*\([^)]*\))?)?\s*[::—]\s*/i },
{ name: 'ticket-letter-prefix', re: /^\s*Ticket\s+[A-Z](?:\s*\([^)]*\))?\s*[::—]\s*/ },
// task #NNN [/ trace-viz §X.Y] [(2026-07-17…)] [rec 22-bis[…]] [phase 2 (pi §2.3)]:
//
// The `/ …` continuation matches compound refs like
// task #201 / trace-viz §4d:
// task #158 / density-spec §3:
// where the reader wanted "here is what the constraint is called in two
// artifact systems". We drop the whole pileup.
//
// Widened 2026-07-18: also accept a small trailing tag word or two before
// the delimiter, and `.` as an end-of-prefix delim in addition to `:`/``/`—`.
// Handles the pi-style `Task #49 lane:`, `Task #225 selfie seam:`,
// `Task #103 P0-4 (2026-07-16).` shapes.
{ name: 'task-num-prefix', re: /^\s*(?:Task|task)\s*#\d+(?:\s*\/\s*[\w §.a-z-]+)?(?:\s+[\w-]+(?:\s+[\w-]+)?)?(?:\s*\([^)]*\))?(?:\s+rec\s+[\d-]+(?:-bis)?)?(?:\s+phase\s+\d+(?:\s*\([^)]*\))?)?\s*[::—.]\s*/ },
// F-N (2026-07-18 e2e audit): prefix
{ name: 'finding-letter-prefix', re: /^\s*F-\d+\s*\([^)]*(?:audit|e2e)[^)]*\)\s*[::—]\s*/i },
// team-lead §X.Y [ruling|正面参照|dispatch verbatim] [(...)]:
{ name: 'team-lead-prefix', re: /^\s*[Tt]eam[- ]lead\s+§[\d.]+(?:\s+\w+)?(?:\s*\([^)]*\))?\s*[::—]\s*/ },
// rec 22-bis [(2026-07-17|pi §X.Y)]:
{ name: 'rec-num-prefix', re: /^\s*rec\s+\d+(?:-bis)?(?:\s+phase\s+\d+)?(?:\s*\([^)]*\))?\s*[::—]\s*/i },
// Round-visual N1 (2026-07-16):
{ name: 'round-visual-prefix', re: /^\s*Round[- ]visual\s+[A-Z]?\d+(?:\s*\([^)]*\))?\s*[::—]\s*/i },
// Clickability audit D3 [fix] (2026-07-17):
{ name: 'audit-batch-prefix', re: /^\s*(?:Clickability\s+audit|hygiene\s+batch|trace[- ]parity\s+batch)\s+[\w\d]+(?:\s+\w+)?(?:\s*\([^)]*\))?\s*[::—]\s*/i },
// 2026-07-XX round-N shot NN: (dated e2e/round prefix)
{ name: 'dated-round-prefix', re: /^\s*20\d{2}-\d{2}-\d{2}\s+(?:round|QA|e2e|老板|walkthrough|trace[- ]parity)[^:]*[::—]\s*/ },
// 2026-07-XX delta: / 2026-07-XX addendum (...): — generic process
// banner starting with a date + one-word tag.
{ name: 'dated-tag-prefix', re: /^\s*20\d{2}-\d{2}-\d{2}\s+\w+(?:\s*\([^)]*\))?\s*[::—]\s*/ },
// Clickability audit fills (...2026-07-17): — audit-batch-prefix widened
// to accept the trailing "fills"/word before the paren+delim.
{ name: 'audit-batch-fills-prefix', re: /^\s*Clickability\s+audit\s+\w+(?:\s*\([^)]*\))?\s*[::—]?\s*/i },
// Density-spec L0 budget: — density-spec followed by L\d level ref
// instead of §N (LangSmith-style level naming).
{ name: 'density-spec-level-prefix', re: /^\s*[Dd]ensity[- ]spec\s+L\d[^:]*[:]\s*/ },
];
// TRAILING (parenthetical) rules. Applied to the whole comment text; may
// match at any position. Non-greedy.
const TRAILING_STRIPPERS = [
// (Fresh-eyes P0 (2026-07-18, review-fresh-eyes.md #N)) — nested variant
{ name: 'paren-fresh-eyes', re: /\s*\(\s*Fresh[- ]eyes\s+P0[^()]*review[- ]fresh[- ]eyes\.md[^()]*\)/gi },
// (review-fresh-eyes.md #N) / (review-*.md #N)
{ name: 'paren-review-md', re: /\s*\(review-[a-z][\w-]*\.md\s*#\d+\)/gi },
// (QA round-N shot NN)
{ name: 'paren-qa-round', re: /\s*\(QA\s+round-\d+(?:\s+shot\s+\d+)?\)/gi },
// (round-N) / (round-N shot NN)
{ name: 'paren-round', re: /\s*\(round-\d+(?:\s+shot\s+\d+)?\)/gi },
// (2026-07-17|老板实测|team-lead §X.Y|正面参照) — mixed process paren
{ name: 'paren-dated-process', re: /\s*\((?:20\d{2}-\d{2}-\d{2})[^()]*(?:老板|team[- ]lead|正面|指令|walkthrough|e2e[- ]audit|round-\d)[^()]*\)/gi },
// (Ticket #N) / (task #N) as trailing tag, incl. trailing tokens like
// (task #103 P0-4, 2026-07-16)
// (task #37 layer 1)
// (2026-07-17, task #49) — leading-date variant
{ name: 'paren-ticket-num', re: /\s*\((?:Ticket|task)\s*#?\d+(?:[,\s][^()]*)?\)/gi },
{ name: 'paren-dated-task', re: /\s*\(20\d{2}-\d{2}-\d{2}\s*,\s*(?:Ticket|task)\s*#?\d+(?:[,\s][^()]*)?\)/gi },
// 205-Δ3 / 205-Δ4 boss-delta bare tag with trailing punctuation
{ name: 'inline-boss-delta', re: /\s*\b205-Δ\d+\b/g },
// (F-1 …)
{ name: 'paren-finding', re: /\s*\(F-\d+(?:\s*[,;]\s*\d{4}-\d{2}-\d{2}[^)]*)?\)/g },
// packages/x/y/z.ts:33-52 → strip. If the coord sat inside its own paren
// `(packages/foo.ts:33-52)`, drop the paren too so we don't leave empty
// "wire () carries..." debris.
{ name: 'upstream-path-paren', re: /\s*\(packages\/[\w-]+\/[\w-]+\/src\/[\w./-]+\.ts:\d+(?:-\d+)?\)/g },
{ name: 'upstream-path', re: /\s*\bpackages\/[\w-]+\/[\w-]+\/src\/[\w./-]+\.ts:\d+(?:-\d+)?/g },
// Compound "QA round-N shot NN" as a bare inline tag (must run BEFORE
// inline-round so we don't leave "QA shot NN" debris).
{ name: 'inline-qa-round-shot', re: /\s*\bQA\s+round-\d+\s+shot\s+\d+\b/gi },
// rec 22-bis / rec 22 as a mid-sentence tag (bare word)
{ name: 'inline-rec', re: /\s*\brec\s+\d+(?:-bis)?/g },
// Round-N tag mid-sentence
{ name: 'inline-round', re: /\s*\bround-\d+\b/gi },
// Orphan closing `)` LEFT AS THE ENTIRE line body after an upstream-path
// strip (source line was `path.ts:295-296):` — the strip removed the path
// but not the parenthesis because it opened on a prior line). Only fires
// when the body reduces to `)` or `):` with no other content.
{ name: 'orphan-close-paren', re: /^\s*\)\s*[:]?\s*$/ },
// Mid-sentence `Ticket #NNN [A-Z] [(2026-…)]` reference — a leftover after
// the leading-prefix strippers didn't fire because the artifact ref sat
// inside a longer sentence like:
// `— Ticket #140 explicitly says "推倒重来"`
// `Ticket #140. Data source is the growth-v2 IPC`
// `raw-inject.js — Ticket #15 B (2026-07-17) envelope:'raw' classifier.`
// Distinct from paren-ticket-num because the artifact ref is bare (no
// enclosing parens). Bare `Ticket #NNN` is an unambiguous internal artifact
// reference (unlike bare `#NNN` which per team-lead (B) 2026-07-18 stays as
// an OSS-verifiable official-repo PR anchor).
{ name: 'inline-ticket-ref', re: /\s*\bTicket\s+#\d+(?:\s+[A-Z])?(?:\s*\([^)]{0,50}\))?(?:\s+(?:step|phase)\s+\d+)?/g },
// Mid-sentence `team-lead §X.Y [tag]` reference.
{ name: 'inline-team-lead-ref', re: /\s*\b[Tt]eam[- ]lead\s+§[\d.]+(?:\s+\w+)?/g },
// Mid-sentence `task #NNN` — the leading-prefix stripper needs a `:`
// delimiter; sentences like `strategy list, task #136` or
// `mergeRecentSessions (task #69 — foo)` leak through.
{ name: 'inline-task-num-ref', re: /\s*[,;]?\s*\btask\s+#\d+\b/g },
];
// INTERNAL doc references. The OSS release pipeline strips
// docs/design-refs/ from the shipped artefact, so a reader who follows a
// `density-spec §N` or `style-guide` reference hits a 404 in the published
// tree. Team-lead directive (2026-07-18): dangling references are worse than
// missing ones — strip both. INTERNAL_DOC_KEEP is intentionally empty; the
// STRIP list covers every internal-only doc pointer we know about.
const INTERNAL_DOC_KEEP = [];
const INTERNAL_DOC_STRIP = [
{ re: /\s*\bpi[- ]agent[- ]ui[- ]study(?:\.md)?\s*§[\d.]+/gi, name: 'pi-study' },
{ re: /\s*\bLangSmith\s+study\s+§[\d.]+(?:\s+rec\s+\d+)?/gi, name: 'langsmith-study' },
{ re: /\s*\btrace[- ]parity(?:\s+batch)?(?:\s+task\s+\d+)?/gi, name: 'trace-parity-batch' },
{ re: /\s*\btrace[- ]viz\s+§[\d.a-z]+/gi, name: 'trace-viz-ref' },
// density-layering spec references — repo-internal doc, stripped from OSS
// artefact so references would 404. Matches `density-spec §N`, `density spec §N.M`.
{ re: /\s*\bdensity[- ]spec\s*§[\d.a-z]+(?:\s*·\s*[\w-]+)?/gi, name: 'density-spec' },
{ re: /\s*\bdensity[- ]layering[- ]spec\s*§[\d.a-z]+/gi, name: 'density-layering-spec' },
// style-guide references — same reason.
{ re: /\s*\bstyle[- ]guide\s*(?:§[\d.a-z]+)?/gi, name: 'style-guide' },
];
// -----------------------------------------------------------------------------
// Comment-aware line rewriter.
//
// A file is edited by walking its comment spans (produced identically to
// comment-sweep-scan.js) and rewriting each span's text. Two shapes:
// - // line comment → text = everything after //
// - /* … */ block → each internal line is treated independently; the
// leading ` * ` (or ` `) prefix is preserved.
//
// After rewriting, the file is emitted with:
// - Empty // lines → line removed entirely (including trailing \n).
// - Empty ` * ` lines in a block comment → line removed if the block still
// has non-empty lines around it; if the whole block becomes empty, the
// entire /* */ span is removed.
// -----------------------------------------------------------------------------
function rewriteCommentText(text) {
const originalLeadingWs = (text.match(/^\s*/) || [''])[0];
let out = text;
// Sentinel-guard PRE-EXISTING empty parens `()` so the paren-cleanup below
// (added to collapse `(density-spec §3)` → gone after INTERNAL_DOC_STRIP
// consumes the content) doesn't clobber legitimate function-call references
// inside comments, e.g. `daemon.ensureUp()` / `refreshSessionList()`.
// Any `()` that survives all strips into the output is one we created; the
// sentinel-restored ones were there in the source and stay.
const EMPTY_PAREN_MARK = 'EMPTYPAREN';
out = out.replace(/\(\s*\)/g, EMPTY_PAREN_MARK);
let leadStripped = false;
// Leading strippers first. Track whether any leading rule fired so cleanup
// knows whether to consume leading whitespace.
for (const rule of LEADING_STRIPPERS) {
const before = out;
out = out.replace(rule.re, '');
if (out !== before) leadStripped = true;
}
// Trailing / inline strippers (do NOT flip leadStripped — they don't touch
// the line's leading whitespace).
for (const rule of TRAILING_STRIPPERS) {
out = out.replace(rule.re, '');
}
for (const rule of INTERNAL_DOC_STRIP) {
const before = out;
out = out.replace(rule.re, '');
// If the strip fired AT THE START of the (possibly-already-partially-
// stripped) text — INTERNAL_DOC_STRIP entries are inline-anywhere, so a
// leading occurrence like `density-spec §4: rows focusable` used to leave
// a `: rows focusable` orphan. Treat leading-position hits as leadStripped
// so the cleanup below eats the stray delimiter.
if (out !== before) {
// Detect leading position by checking whether the pre-strip text started
// (after any whitespace) with a match of this same rule.
const leadingRe = new RegExp('^' + rule.re.source.replace(/^\\s\*/, '\\s*'), rule.re.flags.replace('g', ''));
if (leadingRe.test(before)) leadStripped = true;
}
}
if (leadStripped) {
// A leading strip fired. Clean up the debris (whitespace + orphan
// delimiters) it may have left, then restore the ORIGINAL leading
// whitespace so a bullet in ` - foo` inside a block comment keeps
// its indent.
out = out.replace(/^\s+/, '').replace(/^[::—]\s*/, '');
if (out) out = originalLeadingWs + out;
}
// Clean up empty parens `()` left behind when a strip consumed the sole
// content of a parenthetical (`(packages/foo.ts:33-52)` → `()`). Guarded
// above: pre-existing empty parens were replaced with a sentinel first.
out = out.replace(/\s*\(\s*\)/g, '');
// Restore any pre-existing empty parens.
out = out.split(EMPTY_PAREN_MARK).join('()');
// Deliberately NOT collapsing " ." → "." — the transform must never touch
// spacing that predates our strip. Leaving occasional " ." debris is fine;
// reviewers can spot and fix by hand, and we don't risk munging code-shape
// comments like `// [a, b, ...]` where the space matters.
if (/^[\s.:,;—]*$/.test(out)) return '';
return out;
}
function rewriteFile(src) {
// Walk char-by-char (mirror of comment-sweep-scan's extractComments) and
// build a new string, replacing each comment's TEXT via rewriteCommentText.
//
// State tracking:
// - inStr = string literal char (' " `) currently open, or null.
// - inRegex = true iff currently inside a /.../ literal.
// - lastMeaningful = last non-whitespace char emitted; used to disambiguate
// `/` as regex-start vs division. If lastMeaningful is one of
// `=(,;!&|?:{}[` or empty (BOF) or after a keyword, `/` starts a regex.
// Otherwise (after ident/number/)/]) it's division.
const N = src.length;
let out = '';
let i = 0;
let inStr = null;
let inRegex = false;
let inRegexClass = false;
let lastMeaningful = '';
const REGEX_START_AFTER = new Set([
'', '=', '(', ',', ';', '!', '&', '|', '?', ':', '{', '}', '[',
'+', '-', '*', '%', '^', '~', '<', '>',
]);
const KEYWORD_BEFORE_REGEX = /(?:^|[^\w$])(?:return|typeof|instanceof|in|of|delete|void|new|throw|yield|await|case|do|else)$/;
function isRegexStartContext() {
if (lastMeaningful === '') return true;
if (REGEX_START_AFTER.has(lastMeaningful)) return true;
// Check for keyword ending at `out`. Cheap: last 10 chars.
const tail = out.slice(-12);
if (KEYWORD_BEFORE_REGEX.test(tail)) return true;
return false;
}
const stripped = { lines: 0, blocks: 0, inlineEdits: 0 };
while (i < N) {
const c = src[i];
const next = src[i + 1];
if (inStr) {
out += c;
if (c === '\\' && i + 1 < N) { out += src[i + 1]; i += 2; continue; }
if (c === inStr) { inStr = null; lastMeaningful = c; }
i++;
continue;
}
if (inRegex) {
out += c;
if (c === '\\' && i + 1 < N) { out += src[i + 1]; i += 2; continue; }
if (c === '[') inRegexClass = true;
else if (c === ']') inRegexClass = false;
else if (c === '/' && !inRegexClass) { inRegex = false; lastMeaningful = c; }
i++;
continue;
}
if (c === "'" || c === '"' || c === '`') {
inStr = c;
out += c;
lastMeaningful = c;
i++;
continue;
}
if (c === '/' && next === '/') {
// Line comment. Find the // start's "leading whitespace on this line".
let lineStart = out.lastIndexOf('\n') + 1;
const leading = out.slice(lineStart);
// Find end of comment (next newline).
let j = i + 2;
while (j < N && src[j] !== '\n') j++;
const commentBody = src.slice(i + 2, j);
const originalWasEmpty = commentBody.trim() === '';
const rewritten = rewriteCommentText(commentBody);
if (originalWasEmpty) {
// Preserve empty `//` separator lines verbatim — the file's author
// put them there as visual padding between paragraphs, not as a
// side-effect of any strip.
out += '//' + commentBody;
} else if (rewritten === '') {
// The line's body was 100% artifact-reference AND non-empty. Drop
// the whole line (including leading indent + trailing newline).
out = out.slice(0, lineStart);
if (src[j] === '\n') j++;
stripped.lines++;
} else {
// The rewritten text already includes whatever leading whitespace
// rewriteCommentText decided to preserve; we just prepend `//`.
const newComment = '//' + rewritten;
if (newComment !== '//' + commentBody) stripped.inlineEdits++;
out += newComment;
}
i = j;
continue;
}
if (c === '/' && next === '*') {
// Block comment. Collect the entire span first.
let j = i + 2;
while (j < N && !(src[j] === '*' && src[j + 1] === '/')) j++;
const raw = src.slice(i + 2, j);
const closeAt = j + 2;
// Split into lines, rewrite each internal line while preserving the
// ` * ` / ` *` / ` ` prefix shape.
const lines = raw.split('\n');
const rewrittenLines = [];
let anyRealText = false;
for (let idx = 0; idx < lines.length; idx++) {
const l = lines[idx];
// Detect ` * ` prefix (typical). Preserve indent + optional " * ".
const m = l.match(/^(\s*)(\*\s?)?(.*)$/);
const indent = m[1] || '';
const star = m[2] || '';
const body = m[3] || '';
if (!body.trim()) {
// Blank comment line — keep as-is (structural padding).
rewrittenLines.push(l);
continue;
}
const rewritten = rewriteCommentText(body);
if (rewritten === '') {
// Line body was 100% artifact reference. Drop it.
// If the next line is also empty or drops, we compress; otherwise
// we emit nothing here.
stripped.blocks++;
continue;
}
rewrittenLines.push(indent + star + rewritten);
anyRealText = true;
}
if (!anyRealText && rewrittenLines.every((ll) => !ll.trim())) {
// The entire block became empty (only structural blanks). Remove
// the whole `/* … */` and its trailing newline if present.
// Also remove the leading indentation on the line the /* opened on.
let lineStart = out.lastIndexOf('\n') + 1;
out = out.slice(0, lineStart);
let k = closeAt;
if (src[k] === '\n') k++;
i = k;
stripped.blocks++;
continue;
}
out += '/*' + rewrittenLines.join('\n') + '*/';
i = closeAt;
continue;
}
// Regex-literal detection: `/` that is not `//` (line comment) nor `/*`
// (block comment) AND appears in a regex-start context begins a `/…/` and
// must be skipped so its internal `"` / `'` chars don't fool the string
// tracker (bug: `/model\s+"([^"]+)"/i` was leaving the walker "in string"
// for the rest of the file).
if (c === '/' && next !== '/' && next !== '*' && isRegexStartContext()) {
inRegex = true;
inRegexClass = false;
out += c;
lastMeaningful = c;
i++;
continue;
}
out += c;
if (!/\s/.test(c)) lastMeaningful = c;
i++;
}
return { src: out, stripped };
}
// -----------------------------------------------------------------------------
// File discovery + CLI
// -----------------------------------------------------------------------------
function listJsFiles(scopes) {
const results = [];
for (const dir of scopes) {
const abs = path.join(ROOT, dir);
if (!fs.existsSync(abs)) continue;
walk(abs, results);
}
return results;
}
function walk(dir, out) {
for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
const p = path.join(dir, ent.name);
if (ent.isDirectory()) { walk(p, out); continue; }
if (!ent.isFile()) continue;
if (!/\.(js|mjs|cjs)$/.test(ent.name)) continue;
out.push(p);
}
}
function main() {
const args = process.argv.slice(2);
const apply = args.includes('--apply');
const dry = args.includes('--dry-run') || !apply;
const scopeArg = (args.find((a) => a.startsWith('--scope=')) || '--scope=all').split('=')[1];
const scopes = SCOPES[scopeArg];
if (!scopes) {
console.error(`unknown --scope=${scopeArg}; use main|renderer|all`);
process.exit(2);
}
const files = listJsFiles(scopes);
let totalLines = 0;
let totalBlocks = 0;
let totalInline = 0;
let touched = 0;
for (const f of files) {
const src = fs.readFileSync(f, 'utf8');
const { src: out, stripped } = rewriteFile(src);
if (out === src) continue;
touched++;
totalLines += stripped.lines;
totalBlocks += stripped.blocks;
totalInline += stripped.inlineEdits;
const rel = path.relative(ROOT, f);
const summary = `(-${stripped.lines} full-line, -${stripped.blocks} block-line, ~${stripped.inlineEdits} inline)`;
if (dry) {
process.stdout.write(`~ ${rel} ${summary}\n`);
} else {
fs.writeFileSync(f, out);
process.stdout.write(`M ${rel} ${summary}\n`);
}
}
process.stdout.write(`---\n`);
process.stdout.write(`${touched} files ${dry ? 'would be' : ''} touched; -${totalLines} full-line deletions, -${totalBlocks} block-line deletions, ~${totalInline} inline edits\n`);
}
if (require.main === module) main();
module.exports = { rewriteCommentText, rewriteFile };