From 0edef4e320afcbc5314188d550706841b26a0c06 Mon Sep 17 00:00:00 2001 From: Shinrai Date: Sat, 8 Aug 2026 20:49:49 -0700 Subject: [PATCH 1/5] fix(header): preserve existing @Last modified by identity unless forced The comparison header always stamped the current runner's git identity into @Last modified by, so any machine whose git config user.name differed from what was recorded in existing headers flagged nearly every file as changed purely from an identity mismatch (1551/1565 in one observed run), even with zero real content change. Add extractHeaderLastModifiedIdentity() to parse the existing @Last modified by line and preserve it in the comparison header unless the new forceLastModifiedAuthorUpdate option (--force-last-modified-author-update) is set, mirroring how @Author/@Email is already gated behind forceAuthorUpdate. Uses a dedicated flag rather than reusing forceAuthorUpdate since the two identities are independently meaningful to refresh. Closes #24 --- README.md | 2 ++ src/cli.mjs | 6 ++++- src/core/fix-headers.mjs | 25 ++++++++++++++++++-- tests/cli.test.vitest.mjs | 5 ++++ tests/core-edge.test.vitest.mjs | 42 ++++++++++++++++++++++++++++++--- 5 files changed, 74 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 710a9be..89d3044 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ Common CLI options: - `--json` - `--sample-output` - `--force-author-update` +- `--force-last-modified-author-update` - `--use-gpg-signer-author` - `--cwd ` - `--input ` @@ -103,6 +104,7 @@ Important options: - `authorEmail?: string` - `company?: string` - appends to `@Author` as `Name ` - `forceAuthorUpdate?: boolean` - force update `@Author`/`@Email` to detected or overridden current values +- `forceLastModifiedAuthorUpdate?: boolean` - force update `@Last modified by` to detected or overridden current values. Without this, an existing header's recorded `@Last modified by` identity is preserved and does not by itself trigger an update just because the running author differs (e.g. a different `git config user.name` than whoever last touched the file) - `useGpgSignerAuthor?: boolean` - use signed-commit UID (`%GS`) for detected `@Author` (includes signer comment when present) - `companyName?: string` (default: `Catalyzed Motivation Inc.`) - `copyrightStartYear?: number` (default: current year) diff --git a/src/cli.mjs b/src/cli.mjs index 72c1338..7a2f2cd 100644 --- a/src/cli.mjs +++ b/src/cli.mjs @@ -23,7 +23,7 @@ import fixHeaders from "./fix-header.mjs"; * @module fix-headers/cli */ -const HELP_TEXT = `fix-headers CLI\n\nUsage:\n fix-headers [options]\n\nOptions:\n -h, --help Show help\n --dry-run Compute changes without writing files\n --json Print JSON output\n --verbose Print updated file paths in summary mode\n --sample-output Show previous/new header sample for changed files\n --force-author-update Always update @Author/@Email to detected/current values\n --use-gpg-signer-author Use signed-commit UID (%GS) for detected @Author\n --cwd Working directory for project detection\n --input Single file or folder input\n --include-folder Include folder (repeatable)\n --exclude-folder Exclude folder name/path (repeatable)\n --include-extension Include extension (repeatable)\n --enable-detector Enable only specific detector (repeatable)\n --disable-detector Disable detector by id (repeatable)\n --project-name Override project name\n --language Override language id\n --project-root Override project root\n --marker Override marker filename\n --author-name Override author name\n --author-email Override author email\n --company Append company suffix to @Author (Name )\n --company-name Override company name\n --copyright-start-year Override copyright start year\n --config Load JSON options file\n\nExamples:\n fix-headers --dry-run --include-folder src\n fix-headers --project-name @scope/pkg --company-name "Catalyzed Motivation Inc."\n`; +const HELP_TEXT = `fix-headers CLI\n\nUsage:\n fix-headers [options]\n\nOptions:\n -h, --help Show help\n --dry-run Compute changes without writing files\n --json Print JSON output\n --verbose Print updated file paths in summary mode\n --sample-output Show previous/new header sample for changed files\n --force-author-update Always update @Author/@Email to detected/current values\n --force-last-modified-author-update Always update @Last modified by to detected/current values\n --use-gpg-signer-author Use signed-commit UID (%GS) for detected @Author\n --cwd Working directory for project detection\n --input Single file or folder input\n --include-folder Include folder (repeatable)\n --exclude-folder Exclude folder name/path (repeatable)\n --include-extension Include extension (repeatable)\n --enable-detector Enable only specific detector (repeatable)\n --disable-detector Disable detector by id (repeatable)\n --project-name Override project name\n --language Override language id\n --project-root Override project root\n --marker Override marker filename\n --author-name Override author name\n --author-email Override author email\n --company Append company suffix to @Author (Name )\n --company-name Override company name\n --copyright-start-year Override copyright start year\n --config Load JSON options file\n\nExamples:\n fix-headers --dry-run --include-folder src\n fix-headers --project-name @scope/pkg --company-name "Catalyzed Motivation Inc."\n`; /** * Converts CLI flag token to camelCase key. @@ -95,6 +95,10 @@ export function parseCliArgs(argv) { options.forceAuthorUpdate = true; continue; } + if (arg === "--force-last-modified-author-update") { + options.forceLastModifiedAuthorUpdate = true; + continue; + } if (arg === "--use-gpg-signer-author") { options.useGpgSignerAuthor = true; continue; diff --git a/src/core/fix-headers.mjs b/src/core/fix-headers.mjs index 34c0773..ae3dce7 100644 --- a/src/core/fix-headers.mjs +++ b/src/core/fix-headers.mjs @@ -29,6 +29,7 @@ import { toDatePayload } from "../utils/time.mjs"; * configFile?: string, * sampleOutput?: boolean, * forceAuthorUpdate?: boolean, + * forceLastModifiedAuthorUpdate?: boolean, * useGpgSignerAuthor?: boolean, * enabledDetectors?: string[], * disabledDetectors?: string[], @@ -129,6 +130,20 @@ function extractHeaderAuthorIdentity(headerText) { }; } +/** + * Extracts original last-modified-by identity from an existing header block. + * @param {string} headerText - Existing header content. + * @returns {{ authorName?: string, authorEmail?: string }} Parsed identity values. + */ +function extractHeaderLastModifiedIdentity(headerText) { + const match = headerText.match(/@Last modified by:\s*(.+?)\s*\(([^)\n]+)\)\s*$/m); + + return { + authorName: match?.[1]?.trim(), + authorEmail: match?.[2]?.trim() + }; +} + /** * Extracts original created-at payload from an existing header block. * @param {string} headerText - Existing header content. @@ -253,6 +268,7 @@ export async function fixHeaders(options = {}) { }); const existingHeaderText = existingHeader ? original.slice(existingHeader.start, existingHeader.end) : ""; const existingIdentity = existingHeaderText.length > 0 ? extractHeaderAuthorIdentity(existingHeaderText) : {}; + const existingLastModifiedIdentity = existingHeaderText.length > 0 ? extractHeaderLastModifiedIdentity(existingHeaderText) : {}; const existingCreatedAt = existingHeaderText.length > 0 ? extractHeaderCreatedAt(existingHeaderText) : null; const existingLastModifiedAt = existingHeaderText.length > 0 ? extractHeaderLastModifiedAt(existingHeaderText) : null; const filesystemDates = await readFileDates(filePath); @@ -270,6 +286,7 @@ export async function fixHeaders(options = {}) { const createdAt = existingCreatedAt || gitCreated || toDatePayload(filesystemDates.createdAt); const comparisonLastModifiedAt = existingLastModifiedAt || gitLastUpdated || toDatePayload(filesystemDates.updatedAt); const shouldForceAuthorUpdate = effectiveOptions.forceAuthorUpdate === true; + const shouldForceLastModifiedAuthorUpdate = effectiveOptions.forceLastModifiedAuthorUpdate === true; const comparisonHeader = buildHeader({ absoluteFilePath: filePath, @@ -284,8 +301,12 @@ export async function fixHeaders(options = {}) { projectName: fileMetadata.projectName, createdByName: shouldForceAuthorUpdate ? fileMetadata.authorName : existingIdentity.authorName || fileMetadata.authorName, createdByEmail: shouldForceAuthorUpdate ? fileMetadata.authorEmail : existingIdentity.authorEmail || fileMetadata.authorEmail, - lastModifiedByName: fileMetadata.authorName, - lastModifiedByEmail: fileMetadata.authorEmail, + lastModifiedByName: shouldForceLastModifiedAuthorUpdate + ? fileMetadata.authorName + : existingLastModifiedIdentity.authorName || fileMetadata.authorName, + lastModifiedByEmail: shouldForceLastModifiedAuthorUpdate + ? fileMetadata.authorEmail + : existingLastModifiedIdentity.authorEmail || fileMetadata.authorEmail, authorName: fileMetadata.authorName, authorEmail: fileMetadata.authorEmail, createdAt, diff --git a/tests/cli.test.vitest.mjs b/tests/cli.test.vitest.mjs index 0e17c0f..e24e637 100644 --- a/tests/cli.test.vitest.mjs +++ b/tests/cli.test.vitest.mjs @@ -89,6 +89,11 @@ describe("cli", () => { expect(parsed.options.forceAuthorUpdate).toBe(true); }); + it("parses force-last-modified-author-update flag", () => { + const parsed = parseCliArgs(["--force-last-modified-author-update"]); + expect(parsed.options.forceLastModifiedAuthorUpdate).toBe(true); + }); + it("parses use-gpg-signer-author flag", () => { const parsed = parseCliArgs(["--use-gpg-signer-author"]); expect(parsed.options.useGpgSignerAuthor).toBe(true); diff --git a/tests/core-edge.test.vitest.mjs b/tests/core-edge.test.vitest.mjs index 8b91a5d..f1ca0a4 100644 --- a/tests/core-edge.test.vitest.mjs +++ b/tests/core-edge.test.vitest.mjs @@ -204,17 +204,18 @@ describe("core edge coverage", () => { } }); - it("preserves original header author and updates only last modified by", async () => { + it("preserves original header author and last-modified identity when nothing else changes", async () => { const workspace = await createWorkspace("core-edge-preserve-author"); + const currentYear = new Date().getFullYear(); try { await writeWorkspaceFile(join(workspace, "package.json"), JSON.stringify({ name: "core-edge-preserve-author" }, null, 2)); await writeWorkspaceFile( join(workspace, "src", "one.mjs"), - `/**\n *\t@Project: core-edge-preserve-author\n *\t@Filename: /src/one.mjs\n *\t@Date: 2026-01-01 00:00:00 +00:00 (1735689600)\n *\t@Author: Original Author\n *\t@Email: \n *\t-----\n *\t@Last modified by: Old Updater (old@example.com)\n *\t@Last modified time: 2026-01-02 00:00:00 +00:00 (1735776000)\n *\t-----\n *\t@Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved.\n */\n\nexport const one = true;\n` + `/**\n *\t@Project: core-edge-preserve-author\n *\t@Filename: /src/one.mjs\n *\t@Date: 2026-01-01 00:00:00 +00:00 (1735689600)\n *\t@Author: Original Author\n *\t@Email: \n *\t-----\n *\t@Last modified by: Old Updater (old@example.com)\n *\t@Last modified time: 2026-01-02 00:00:00 +00:00 (1735776000)\n *\t-----\n *\t@Copyright: Copyright (c) ${currentYear}-${currentYear} Catalyzed Motivation Inc. All rights reserved.\n */\n\nexport const one = true;\n` ); - await coreFixHeaders({ + const result = await coreFixHeaders({ cwd: workspace, input: "src/one.mjs", authorName: "New Updater", @@ -222,10 +223,45 @@ describe("core edge coverage", () => { dryRun: false }); + const updated = await readFile(join(workspace, "src", "one.mjs"), "utf8"); + expect(updated).toContain("@Author: Original Author"); + expect(updated).toContain("@Email: "); + expect(updated).toContain("@Last modified by: Old Updater (old@example.com)"); + expect(updated).toContain("@Last modified time: 2026-01-02 00:00:00 +00:00 (1735776000)"); + expect(result.filesUpdated).toBe(0); + } finally { + await cleanupWorkspace(workspace); + } + }); + + it("forces last-modified identity update when enabled without forcing created-by", async () => { + const workspace = await createWorkspace("core-edge-force-last-modified-author-update"); + + try { + await writeWorkspaceFile( + join(workspace, "package.json"), + JSON.stringify({ name: "core-edge-force-last-modified-author-update" }, null, 2) + ); + await writeWorkspaceFile( + join(workspace, "src", "one.mjs"), + `/**\n *\t@Project: core-edge-force-last-modified-author-update\n *\t@Filename: /src/one.mjs\n *\t@Date: 2026-01-01 00:00:00 +00:00 (1735689600)\n *\t@Author: Original Author\n *\t@Email: \n *\t-----\n *\t@Last modified by: Old Updater (old@example.com)\n *\t@Last modified time: 2026-01-02 00:00:00 +00:00 (1735776000)\n *\t-----\n *\t@Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved.\n */\n\nexport const one = true;\n` + ); + + const result = await coreFixHeaders({ + cwd: workspace, + input: "src/one.mjs", + authorName: "New Updater", + authorEmail: "new@example.com", + forceLastModifiedAuthorUpdate: true, + dryRun: false + }); + const updated = await readFile(join(workspace, "src", "one.mjs"), "utf8"); expect(updated).toContain("@Author: Original Author"); expect(updated).toContain("@Email: "); expect(updated).toContain("@Last modified by: New Updater (new@example.com)"); + expect(updated).not.toContain("@Last modified time: 2026-01-02 00:00:00 +00:00 (1735776000)"); + expect(result.filesUpdated).toBe(1); } finally { await cleanupWorkspace(workspace); } From efc50de29016ab962842718b3e278f001de47494 Mon Sep 17 00:00:00 2001 From: Shinrai Date: Sat, 8 Aug 2026 21:33:42 -0700 Subject: [PATCH 2/5] test(core-edge): make force-last-modified-update fixture copyright year dynamic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fixture header hard-coded a 2013-2026 copyright range, which mismatches the tool's default (current year) regardless of forceLastModifiedAuthorUpdate — the test would still report filesUpdated: 1 even if the force flag were a no-op, since the copyright-line mismatch alone forces the rewrite. Match the sibling preservation test's pattern of a dynamic current-year range so the force flag is the only reason the header changes. Addresses review comment on PR #25. --- tests/core-edge.test.vitest.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/core-edge.test.vitest.mjs b/tests/core-edge.test.vitest.mjs index f1ca0a4..c474ec6 100644 --- a/tests/core-edge.test.vitest.mjs +++ b/tests/core-edge.test.vitest.mjs @@ -236,6 +236,7 @@ describe("core edge coverage", () => { it("forces last-modified identity update when enabled without forcing created-by", async () => { const workspace = await createWorkspace("core-edge-force-last-modified-author-update"); + const currentYear = new Date().getFullYear(); try { await writeWorkspaceFile( @@ -244,7 +245,7 @@ describe("core edge coverage", () => { ); await writeWorkspaceFile( join(workspace, "src", "one.mjs"), - `/**\n *\t@Project: core-edge-force-last-modified-author-update\n *\t@Filename: /src/one.mjs\n *\t@Date: 2026-01-01 00:00:00 +00:00 (1735689600)\n *\t@Author: Original Author\n *\t@Email: \n *\t-----\n *\t@Last modified by: Old Updater (old@example.com)\n *\t@Last modified time: 2026-01-02 00:00:00 +00:00 (1735776000)\n *\t-----\n *\t@Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved.\n */\n\nexport const one = true;\n` + `/**\n *\t@Project: core-edge-force-last-modified-author-update\n *\t@Filename: /src/one.mjs\n *\t@Date: 2026-01-01 00:00:00 +00:00 (1735689600)\n *\t@Author: Original Author\n *\t@Email: \n *\t-----\n *\t@Last modified by: Old Updater (old@example.com)\n *\t@Last modified time: 2026-01-02 00:00:00 +00:00 (1735776000)\n *\t-----\n *\t@Copyright: Copyright (c) ${currentYear}-${currentYear} Catalyzed Motivation Inc. All rights reserved.\n */\n\nexport const one = true;\n` ); const result = await coreFixHeaders({ From e067f9de0a4b33a55286beebb90d81f41e1ce0be Mon Sep 17 00:00:00 2001 From: Shinrai Date: Sat, 8 Aug 2026 22:28:19 -0700 Subject: [PATCH 3/5] fix(header): stop forced author updates from cascading into last-modified identity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit forceLastModifiedAuthorUpdate only gated the comparison header used to decide whether a rewrite was needed. Once any other reason triggered needsUpdate (e.g. forceAuthorUpdate forcing createdBy to differ, or a malformed/missing @Last modified time repair), the actual rewritten header unconditionally stamped the current runner's identity into @Last modified by, bypassing the flag entirely and defeating the point of splitting it from forceAuthorUpdate. Apply the same preserve-unless-forced gate to the rewrite branch. @Last modified time still always refreshes to now on any real rewrite — only the identity is gated. Addresses suppressed review finding on PR #25. --- src/core/fix-headers.mjs | 8 ++++++-- tests/core-edge.test.vitest.mjs | 28 ++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/core/fix-headers.mjs b/src/core/fix-headers.mjs index ae3dce7..77593f0 100644 --- a/src/core/fix-headers.mjs +++ b/src/core/fix-headers.mjs @@ -340,8 +340,12 @@ export async function fixHeaders(options = {}) { projectName: fileMetadata.projectName, createdByName: shouldForceAuthorUpdate ? fileMetadata.authorName : existingIdentity.authorName || fileMetadata.authorName, createdByEmail: shouldForceAuthorUpdate ? fileMetadata.authorEmail : existingIdentity.authorEmail || fileMetadata.authorEmail, - lastModifiedByName: fileMetadata.authorName, - lastModifiedByEmail: fileMetadata.authorEmail, + lastModifiedByName: shouldForceLastModifiedAuthorUpdate + ? fileMetadata.authorName + : existingLastModifiedIdentity.authorName || fileMetadata.authorName, + lastModifiedByEmail: shouldForceLastModifiedAuthorUpdate + ? fileMetadata.authorEmail + : existingLastModifiedIdentity.authorEmail || fileMetadata.authorEmail, authorName: fileMetadata.authorName, authorEmail: fileMetadata.authorEmail, createdAt, diff --git a/tests/core-edge.test.vitest.mjs b/tests/core-edge.test.vitest.mjs index c474ec6..5b5f058 100644 --- a/tests/core-edge.test.vitest.mjs +++ b/tests/core-edge.test.vitest.mjs @@ -296,6 +296,34 @@ describe("core edge coverage", () => { } }); + it("does not cascade a forced author update into the last-modified identity of an unrelated rewrite", async () => { + const workspace = await createWorkspace("core-edge-force-author-no-cascade"); + + try { + await writeWorkspaceFile(join(workspace, "package.json"), JSON.stringify({ name: "core-edge-force-author-no-cascade" }, null, 2)); + await writeWorkspaceFile( + join(workspace, "src", "one.mjs"), + `/**\n *\t@Project: core-edge-force-author-no-cascade\n *\t@Filename: /src/one.mjs\n *\t@Date: 2026-01-01 00:00:00 +00:00 (1735689600)\n *\t@Author: Original Author\n *\t@Email: \n *\t-----\n *\t@Last modified by: Old Updater (old@example.com)\n *\t@Last modified time: 2026-01-02 00:00:00 +00:00 (1735776000)\n *\t-----\n *\t@Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc All rights reserved.\n */\n\nexport const one = true;\n` + ); + + await coreFixHeaders({ + cwd: workspace, + input: "src/one.mjs", + authorName: "Forced Author", + authorEmail: "forced@example.com", + forceAuthorUpdate: true, + dryRun: false + }); + + const updated = await readFile(join(workspace, "src", "one.mjs"), "utf8"); + expect(updated).toContain("@Author: Forced Author"); + expect(updated).toContain("@Last modified by: Old Updater (old@example.com)"); + expect(updated).not.toContain("@Last modified time: 2026-01-02 00:00:00 +00:00 (1735776000)"); + } finally { + await cleanupWorkspace(workspace); + } + }); + it("applies company suffix to generated author line", async () => { const workspace = await createWorkspace("core-edge-author-company"); From 8a2e4d89e532c568fab1e9d0600326a2553420a3 Mon Sep 17 00:00:00 2001 From: Shinrai Date: Sat, 8 Aug 2026 22:57:55 -0700 Subject: [PATCH 4/5] types(core): regenerate fix-headers.d.mts for forceLastModifiedAuthorUpdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The published TypeScript declarations were out of sync with the JSDoc source after adding forceLastModifiedAuthorUpdate to FixHeadersOptions, which would have produced type errors for TS consumers using the new flag. Regenerated via the project's own types:build script and kept only the line this PR's change touches — the regeneration also surfaced unrelated pre-existing drift in constants.d.mts, file-discovery.d.mts, and detectors/index.d.mts from other features already merged into next, which is out of scope here and left untouched. Addresses suppressed review finding on PR #25. --- types/src/core/fix-headers.d.mts | 1 + 1 file changed, 1 insertion(+) diff --git a/types/src/core/fix-headers.d.mts b/types/src/core/fix-headers.d.mts index bd3fc93..6d2ea4a 100644 --- a/types/src/core/fix-headers.d.mts +++ b/types/src/core/fix-headers.d.mts @@ -15,6 +15,7 @@ export type FixHeadersOptions = { configFile?: string; sampleOutput?: boolean; forceAuthorUpdate?: boolean; + forceLastModifiedAuthorUpdate?: boolean; useGpgSignerAuthor?: boolean; enabledDetectors?: string[]; disabledDetectors?: string[]; From b862a908d059b290cf7324b22d4ffbb958addaf7 Mon Sep 17 00:00:00 2001 From: Shinrai Date: Sat, 8 Aug 2026 23:01:52 -0700 Subject: [PATCH 5/5] types(core): regenerate full declaration output, not a hand-trimmed subset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit manually stripped the gitignore-related lines out of the tsc-regenerated declarations to keep the diff scoped to this PR's own change. That was wrong: declaration output is fully deterministic from source, so trimming it just means the file ships already stale the moment this PR merges (missing gitignore, which this branch's own source already declares). Regenerate the full, untrimmed output via npm run types:build instead — picking up the pre-existing gitignore/root-anchor-ignores (74c34da) declaration drift alongside this PR's forceLastModifiedAuthorUpdate addition, so next isn't left stale in either direction regardless of merge order. --- types/src/constants.d.mts | 24 ++++++++++++++++++++++-- types/src/core/file-discovery.d.mts | 7 +++++-- types/src/core/fix-headers.d.mts | 1 + types/src/detectors/index.d.mts | 3 ++- 4 files changed, 30 insertions(+), 5 deletions(-) diff --git a/types/src/constants.d.mts b/types/src/constants.d.mts index 3c8b3e9..c39eb54 100644 --- a/types/src/constants.d.mts +++ b/types/src/constants.d.mts @@ -1,7 +1,27 @@ /** @type {string} */ export const DEFAULT_COMPANY_NAME: string; -/** @type {number} */ +/** Header must sit near the top of the file, but a metadata block can legitimately + * run long; cap the scan generously so a long block's closing `*​/` is still seen. + * @type {number} */ export const DEFAULT_MAX_HEADER_SCAN_LINES: number; -/** @type {Set} */ +/** + * Folders skipped at ANY depth — vendored / VCS directories that are never source and can + * legitimately nest (hoisted `node_modules`, submodule `.git`). + * @type {Set} + */ +export const ALWAYS_IGNORE_FOLDERS: Set; +/** + * Folders skipped ONLY at the project root — build / cache output directories. Anchored to + * the root so a nested SOURCE directory that happens to share the name (e.g. `tools/build`, + * `packages/x/dist`-style source) is still processed; only the top-level `/build`, `/dist`, + * `/coverage`, … are ignored. + * @type {Set} + */ +export const ROOT_IGNORE_FOLDERS: Set; +/** + * Backward-compatible union of {@link ALWAYS_IGNORE_FOLDERS} and {@link ROOT_IGNORE_FOLDERS}. + * Discovery applies the two sets with different scoping; prefer the specific sets. + * @type {Set} + */ export const DEFAULT_IGNORE_FOLDERS: Set; export { DETECTOR_PROFILES, getAllowedExtensions, getEnabledDetectors } from "./detectors/index.mjs"; diff --git a/types/src/core/file-discovery.d.mts b/types/src/core/file-discovery.d.mts index db289cb..b364b62 100644 --- a/types/src/core/file-discovery.d.mts +++ b/types/src/core/file-discovery.d.mts @@ -7,8 +7,10 @@ * enabledDetectors?: string[], * disabledDetectors?: string[], * includeFolders?: string[], - * excludeFolders?: string[] - * }} options - File discovery options. + * excludeFolders?: string[], + * gitignore?: boolean | string | string[] + * }} options - File discovery options. `gitignore`: `false` disables; a path or array of + * paths loads those ignore files; anything else / omitted auto-detects `/.gitignore`. * @returns {Promise} Absolute file paths. */ export function discoverFiles(options: { @@ -19,4 +21,5 @@ export function discoverFiles(options: { disabledDetectors?: string[]; includeFolders?: string[]; excludeFolders?: string[]; + gitignore?: boolean | string | string[]; }): Promise; diff --git a/types/src/core/fix-headers.d.mts b/types/src/core/fix-headers.d.mts index 6d2ea4a..da4649b 100644 --- a/types/src/core/fix-headers.d.mts +++ b/types/src/core/fix-headers.d.mts @@ -29,6 +29,7 @@ export type FixHeadersOptions = { includeFolders?: string[]; excludeFolders?: string[]; includeExtensions?: string[]; + gitignore?: boolean | string | string[]; projectName?: string; language?: string; projectRoot?: string; diff --git a/types/src/detectors/index.d.mts b/types/src/detectors/index.d.mts index 3a932d7..d8aa5d9 100644 --- a/types/src/detectors/index.d.mts +++ b/types/src/detectors/index.d.mts @@ -26,13 +26,14 @@ export function getDetectorById(id: string): (typeof DETECTOR_PROFILES)[number] /** * Resolves comment syntax for a file path using detector-specific templates. * @param {string} filePath - File path. - * @param {{ language?: string, enabledDetectors?: string[], disabledDetectors?: string[], detectorSyntaxOverrides?: Record }} [options={}] - Runtime options. + * @param {{ language?: string, enabledDetectors?: string[], disabledDetectors?: string[], detectors?: DetectorProfile[], detectorSyntaxOverrides?: Record }} [options={}] - Runtime options. `detectors` overrides the enabled-detector set (matching {@link detectProjectFromMarkers}). * @returns {{kind: "block" | "line" | "html", linePrefix?: string, lineSeparator?: string, blockStart?: string, blockLinePrefix?: string, blockEnd?: string}} Syntax descriptor. */ export function getCommentSyntaxForFile(filePath: string, options?: { language?: string; enabledDetectors?: string[]; disabledDetectors?: string[]; + detectors?: DetectorProfile[]; detectorSyntaxOverrides?: Record