Skip to content

Commit dfaaa86

Browse files
fix(core): harden audited edge cases
1 parent 71cdc04 commit dfaaa86

12 files changed

Lines changed: 76 additions & 20 deletions

File tree

docs/releases/v0.10.0-implementation-ledger.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ that npm, GitHub `main`, or the public Action already contains the fix.
2424
| #640 prior FixMap/setup artifacts | implemented | Contract-recognized report/context/verify artifacts and signature-recognized setup commands are removed from the analysis file and changed-file snapshots before reporting. Same-path team-owned content remains eligible. Artifact and scan-to-analysis tests cover both sides. |
2525
| #641 routed-test attribution | implemented | Impact routing first attributes a test to the ranked seed it actually imports, then uses path proximity only as a fallback. A regression with an earlier unrelated `data.json` seed proves the test remains attributed to its imported source. |
2626
| #642 Windows npm doctor shims | implemented | Doctor collapses same-directory extensionless/`.cmd`/`.ps1` npm launchers into one installation while retaining identical launchers from different directories as a real conflict. Focused doctor tests pass. |
27+
| Post-8446838 correctness sweep | implemented | Six claims from an external 100-item audit survived live-branch reproduction and now have focused regressions: diagnostic truncation backs off rather than splitting UTF-16 surrogate pairs; tsconfig aliases preserve an empty wildcard capture; `sk-` secrets ending in `-` redact correctly; cached and parsed history accepts exact 40-character SHA-1 or 64-character SHA-256 identities; explicit single-ref diffs no longer absorb unrelated untracked files (working-tree inclusion remains separately opt-in); and remote Git refs cannot start with `/`. Stale, speculative, and disproven claims from that document were not treated as bugs. |
2728
| Scheduled external baseline snapshot | implemented | The 2026-08-24 `external-eval` log confirms both FixMap gates passed and only `evaluate-baseline --suite external --check-recorded` failed on `main`. The branch's deterministic-evidence work refreshes that artifact. After the remaining language and scan corrections, a full 16-repository record changed only six lower-ranked webpack evidence scalars; every Top-5 path and aggregate hit rate stayed unchanged, and two subsequent filtered webpack runs were byte-identical. A clean Linux check remains a candidate gate. This repairs snapshot drift, not a general benchmark claim. |
2829
| Nested-checkout history scope | implemented | A generated-example freshness gate exposed that a scan rooted below a parent Git checkout read the parent-wide commit log and could confuse matching parent-relative filenames with repository-relative identities. History counts and names are now path-limited and `--relative` to the scanned root; a nested-repository regression proves unrelated parent commits are absent. |
2930
| PR CI and v0.10 stress campaign | verified | Draft PR #645 runs the real GitHub matrix; current checkpoints through run 33295260500 pass the comprehensive `npm run ci` job plus Node 20.11/22 Ubuntu and Node 24 macOS/Windows jobs. Core and CLI Vitest configs bound file-level workers and retain 15-second default integration/hook timeouts. A sustained 94%-CPU Windows run reproduced seven Git/cache tests at their 30-second ceiling; those named heavy cases now allow 60 seconds only on Windows and remain 30 seconds elsewhere, after which the isolated group and the complete 736-test Core suite passed without cache races. The MCP wire stress retains 10 seconds off Windows and allows 30 seconds for Windows process startup. `npm run stress:v0.10` proves four concurrent cold analyses are deterministic, exact warm reuse is faster, corrupt indexes heal without stale source, saved reports stay isolated, outside links are not scanned, and MCP returns `-32002`/`-32700` on the wire. The scanner benchmark now requires both its completion marker and fixture directory before reuse, preventing a stale marker from producing a false zero-file failure. A final uninterrupted local `npm run ci` passes 42 Action, 313 CLI, 736 Core, and 4 web tests plus audit, lint, production build, generated artifacts, smoke, stress, study integrity, retrieval, adversarial, and the 1,000-file scanner bound. Clean-package tarball proof and the broader release-candidate matrix remain separate gates. |

packages/action/dist/index.mjs

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -814,7 +814,7 @@ function isDistinctiveFragment(fragment) {
814814
return punctuationCount >= 1 && /[\p{L}\p{N}]/u.test(fragment);
815815
}
816816
function redactSensitiveTaskText(text) {
817-
return text.replace(/(https?:\/\/)[^/\s@]+@/gi, "$1").replace(/\b(?:ghp|gho|ghu|ghs|github_pat)_[A-Za-z0-9_]{8,}\b/g, "[redacted]").replace(/\bAKIA[0-9A-Z]{16}\b/g, "[redacted]").replace(/\bsk-[A-Za-z0-9_-]{16,}\b/g, "[redacted]");
817+
return text.replace(/(https?:\/\/)[^/\s@]+@/gi, "$1").replace(/\b(?:ghp|gho|ghu|ghs|github_pat)_[A-Za-z0-9_]{8,}\b/g, "[redacted]").replace(/\bAKIA[0-9A-Z]{16}\b/g, "[redacted]").replace(/\bsk-[A-Za-z0-9_-]{16,}(?=$|[^A-Za-z0-9_])/g, "[redacted]");
818818
}
819819
function stripHttpUrls(text) {
820820
return text.includes("://") ? text.replace(/https?:\/\/[^\s<>()\[\]{}]+/gi, " [url] ") : text;
@@ -2836,7 +2836,8 @@ function resolveSpecifier(fromPath, specifier, repoPaths, aliases, workspacePack
28362836
for (const alias of aliases) {
28372837
if (!specifier.startsWith(alias.prefix) || !specifier.endsWith(alias.suffix))
28382838
continue;
2839-
const middle = specifier.slice(alias.prefix.length, specifier.length - alias.suffix.length || void 0);
2839+
const end = alias.suffix.length === 0 ? specifier.length : specifier.length - alias.suffix.length;
2840+
const middle = specifier.slice(alias.prefix.length, end);
28402841
roots.push(...alias.targets.map((target) => target.replace("*", middle)));
28412842
}
28422843
}
@@ -4081,7 +4082,14 @@ function stripByteOrderMark(value) {
40814082
return value.replace(/^\uFEFF/, "");
40824083
}
40834084
function truncateForDiagnostic(value, limit) {
4084-
return value.length <= limit ? value : `${value.slice(0, limit)}\u2026`;
4085+
if (value.length <= limit)
4086+
return value;
4087+
let end = Math.max(0, limit);
4088+
const last = value.charCodeAt(end - 1);
4089+
const next = value.charCodeAt(end);
4090+
if (last >= 55296 && last <= 56319 && next >= 56320 && next <= 57343)
4091+
end -= 1;
4092+
return `${value.slice(0, end)}\u2026`;
40854093
}
40864094

40874095
// packages/core/dist/semantic.js
@@ -5828,7 +5836,7 @@ function isCachedHistory(candidate) {
58285836
return false;
58295837
}
58305838
return candidate.commits.every((commit) => {
5831-
if (!isRecord5(commit) || typeof commit.hash !== "string" || !/^[a-f0-9]{40}$/i.test(commit.hash) || typeof commit.committedAt !== "number" || !Number.isSafeInteger(commit.committedAt) || commit.committedAt < 0 || commit.author !== void 0 && (typeof commit.author !== "string" || !commit.author.trim() || commit.author.length > 200 || /[\0-\x1f\x7f]/.test(commit.author)) || !Array.isArray(commit.files))
5839+
if (!isRecord5(commit) || typeof commit.hash !== "string" || !/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/i.test(commit.hash) || typeof commit.committedAt !== "number" || !Number.isSafeInteger(commit.committedAt) || commit.committedAt < 0 || commit.author !== void 0 && (typeof commit.author !== "string" || !commit.author.trim() || commit.author.length > 200 || /[\0-\x1f\x7f]/.test(commit.author)) || !Array.isArray(commit.files))
58325840
return false;
58335841
return commit.files.every(isCachedRelativePath);
58345842
});
@@ -6397,8 +6405,7 @@ async function readDiff(repoRoot, diffSpec, diagnostics, internalPaths) {
63976405
exec("git", ["diff", "--relative", diffSpec, ...gitPathspec(internalPaths)], { cwd: repoRoot, maxBuffer: GIT_MAX_BUFFER })
63986406
]);
63996407
const tracked = names.split(/\r?\n/).map((path) => path.trim()).filter(Boolean).map(normalizePath3);
6400-
const untracked = diffSpec.includes("..") ? [] : await listUntrackedPaths(repoRoot, internalPaths);
6401-
const changedFiles = [.../* @__PURE__ */ new Set([...tracked, ...untracked])].sort((a, b) => a.localeCompare(b));
6408+
const changedFiles = [...new Set(tracked)].sort((a, b) => a.localeCompare(b));
64026409
diagnostics.push({
64036410
code: "diff-resolved",
64046411
severity: "info",
@@ -6568,7 +6575,7 @@ function parseHistoryLog(logText, repositoryPaths) {
65686575
const committedAt = Number.parseInt(header.slice(separator + 1, secondSeparator === -1 ? void 0 : secondSeparator).trim(), 10);
65696576
const rawAuthor = secondSeparator === -1 ? "" : header.slice(secondSeparator + 1).trim();
65706577
const author = rawAuthor && !/[\0-\x1f\x7f]/.test(rawAuthor) ? rawAuthor.slice(0, 200) : void 0;
6571-
if (!/^[a-f0-9]{40}$/i.test(hash) || !Number.isSafeInteger(committedAt) || committedAt < 0)
6578+
if (!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/i.test(hash) || !Number.isSafeInteger(committedAt) || committedAt < 0)
65726579
continue;
65736580
inspectedCommits += 1;
65746581
const allFiles = [...new Set(fields.map((path) => path.replace(/^\r?\n/, "")).filter(Boolean).map(normalizePath3))];

packages/cli/src/repository-source.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export type ParsedGitHubIssueSource = {
102102
export function isSafeGitRefName(value: string): boolean {
103103
return value.length > 0 && value.length <= 255 &&
104104
value !== "@" &&
105-
!/^[.-]|[./]$|[\s\u0000-\u001f\u007f~^:?*\[\\]|\.\.|\/\/|@\{|(?:^|\/)\.|(?:^|\/)[^/]*\.lock(?:\/|$)/i.test(value);
105+
!/^[.\/-]|[./]$|[\s\u0000-\u001f\u007f~^:?*\[\\]|\.\.|\/\/|@\{|(?:^|\/)\.|(?:^|\/)[^/]*\.lock(?:\/|$)/i.test(value);
106106
}
107107

108108
export type PublicGitHubIssue = {

packages/cli/test/mcp.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1128,6 +1128,10 @@ describe("MCP surface parity", () => {
11281128
success: true,
11291129
value: { issue: "x", repo: "https://github.com/o/r", ref: "release-2.x" }
11301130
});
1131+
expect(parsePlanArguments({ issue: "x", repo: "https://github.com/o/r", ref: "/main" })).toEqual({
1132+
success: false,
1133+
message: '"ref" must be a safe branch or tag name.'
1134+
});
11311135
});
11321136

11331137
it("rejects unknown compare arguments at the request handler", async () => {

packages/core/src/import-graph.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -693,7 +693,8 @@ function resolveSpecifier(
693693
roots.push(...(workspacePackages.get(specifier) ?? []));
694694
for (const alias of aliases) {
695695
if (!specifier.startsWith(alias.prefix) || !specifier.endsWith(alias.suffix)) continue;
696-
const middle = specifier.slice(alias.prefix.length, specifier.length - alias.suffix.length || undefined);
696+
const end = alias.suffix.length === 0 ? specifier.length : specifier.length - alias.suffix.length;
697+
const middle = specifier.slice(alias.prefix.length, end);
697698
roots.push(...alias.targets.map((target) => target.replace("*", middle)));
698699
}
699700
}

packages/core/src/repo-scan.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,7 @@ function isCachedHistory(candidate: unknown): candidate is RepositoryHistory {
376376
return false;
377377
}
378378
return candidate.commits.every((commit) => {
379-
if (!isRecord(commit) || typeof commit.hash !== "string" || !/^[a-f0-9]{40}$/i.test(commit.hash) ||
379+
if (!isRecord(commit) || typeof commit.hash !== "string" || !/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/i.test(commit.hash) ||
380380
typeof commit.committedAt !== "number" || !Number.isSafeInteger(commit.committedAt) || commit.committedAt < 0 ||
381381
(commit.author !== undefined && (typeof commit.author !== "string" || !commit.author.trim() || commit.author.length > 200 || /[\0-\x1f\x7f]/.test(commit.author))) ||
382382
!Array.isArray(commit.files)) return false;
@@ -1225,8 +1225,7 @@ async function readDiff(
12251225
.map((path) => path.trim())
12261226
.filter(Boolean)
12271227
.map(normalizePath);
1228-
const untracked = diffSpec.includes("..") ? [] : await listUntrackedPaths(repoRoot, internalPaths);
1229-
const changedFiles = [...new Set([...tracked, ...untracked])].sort((a, b) => a.localeCompare(b));
1228+
const changedFiles = [...new Set(tracked)].sort((a, b) => a.localeCompare(b));
12301229
diagnostics.push({
12311230
code: "diff-resolved",
12321231
severity: "info",
@@ -1458,7 +1457,7 @@ async function readRepositoryHistory(
14581457
}
14591458
}
14601459

1461-
function parseHistoryLog(
1460+
export function parseHistoryLog(
14621461
logText: string,
14631462
repositoryPaths: ReadonlySet<string>
14641463
): { commits: HistoryCommit[]; inspectedCommits: number; skippedLargeCommits: number } {
@@ -1477,7 +1476,7 @@ function parseHistoryLog(
14771476
const committedAt = Number.parseInt(header.slice(separator + 1, secondSeparator === -1 ? undefined : secondSeparator).trim(), 10);
14781477
const rawAuthor = secondSeparator === -1 ? "" : header.slice(secondSeparator + 1).trim();
14791478
const author = rawAuthor && !/[\0-\x1f\x7f]/.test(rawAuthor) ? rawAuthor.slice(0, 200) : undefined;
1480-
if (!/^[a-f0-9]{40}$/i.test(hash) || !Number.isSafeInteger(committedAt) || committedAt < 0) continue;
1479+
if (!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/i.test(hash) || !Number.isSafeInteger(committedAt) || committedAt < 0) continue;
14811480

14821481
inspectedCommits += 1;
14831482
const allFiles = [...new Set(fields

packages/core/src/signals.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ export function redactSensitiveTaskText(text: string): string {
298298
.replace(/(https?:\/\/)[^/\s@]+@/gi, "$1")
299299
.replace(/\b(?:ghp|gho|ghu|ghs|github_pat)_[A-Za-z0-9_]{8,}\b/g, "[redacted]")
300300
.replace(/\bAKIA[0-9A-Z]{16}\b/g, "[redacted]")
301-
.replace(/\bsk-[A-Za-z0-9_-]{16,}\b/g, "[redacted]");
301+
.replace(/\bsk-[A-Za-z0-9_-]{16,}(?=$|[^A-Za-z0-9_])/g, "[redacted]");
302302
}
303303

304304
/** URLs are never identifier evidence. Removing them before exact-fragment and identifier

packages/core/src/text.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,10 @@ export function stripByteOrderMark(value: string): string {
1616
}
1717

1818
export function truncateForDiagnostic(value: string, limit: number): string {
19-
return value.length <= limit ? value : `${value.slice(0, limit)}…`;
19+
if (value.length <= limit) return value;
20+
let end = Math.max(0, limit);
21+
const last = value.charCodeAt(end - 1);
22+
const next = value.charCodeAt(end);
23+
if (last >= 0xd800 && last <= 0xdbff && next >= 0xdc00 && next <= 0xdfff) end -= 1;
24+
return `${value.slice(0, end)}…`;
2025
}

packages/core/test/import-graph.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,16 @@ describe("buildImportGraph", () => {
6767
expect([...(graph.imports.get("apps/web/page.ts") ?? [])]).toEqual(["packages/auth/src/reset.ts"]);
6868
});
6969

70+
it("resolves an alias whose wildcard captures an empty middle", () => {
71+
const graph = buildImportGraph([
72+
codeFile("app.ts", 'import { value } from "foo";'),
73+
codeFile("tsconfig.json", JSON.stringify({ compilerOptions: { paths: { "*foo": ["src/prefix*"] } } })),
74+
codeFile("src/prefix.ts", "export const value = true;")
75+
]);
76+
77+
expect([...(graph.imports.get("app.ts") ?? [])]).toEqual(["src/prefix.ts"]);
78+
});
79+
7080
it("resolves Python relative, package, and imported-module relationships", () => {
7181
const files = [
7282
codeFile("services/auth/app/api/reset.py", [

packages/core/test/repo-scan.test.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { tmpdir } from "node:os";
44
import { join, resolve } from "node:path";
55
import { promisify } from "node:util";
66
import { describe, expect, it } from "vitest";
7-
import { scanRepo, summarizeSkippedScope } from "../src/repo-scan.js";
7+
import { parseHistoryLog, scanRepo, summarizeSkippedScope } from "../src/repo-scan.js";
88

99
const exec = promisify(execFile);
1010
const HEAVY_GIT_TEST_TIMEOUT = process.platform === "win32" ? 60_000 : 30_000;
@@ -251,7 +251,7 @@ describe("scanRepo", () => {
251251
expect(repo.diagnostics[0]?.code).toBe("diff-unavailable");
252252
});
253253

254-
it("includes untracked files as changed for working-tree diff specs", { timeout: 30_000 }, async () => {
254+
it("keeps untracked files out of an explicit single-ref diff", { timeout: 30_000 }, async () => {
255255
const root = await mkdtemp(join(tmpdir(), "fixmap-untracked-"));
256256
await mkdir(join(root, "src"), { recursive: true });
257257
await mkdir(join(root, "api"), { recursive: true });
@@ -267,7 +267,7 @@ describe("scanRepo", () => {
267267
const repo = await scanRepo({ repoRoot: root, diffSpec: "HEAD" });
268268

269269
expect(repo.changedFiles).toContain("src/login.ts");
270-
expect(repo.changedFiles).toContain("api/index.ts");
270+
expect(repo.changedFiles).not.toContain("api/index.ts");
271271
});
272272

273273
it("maps tracked edits in working-tree mode and leaves untracked files out", { timeout: 30_000 }, async () => {
@@ -1169,6 +1169,19 @@ describe("scanRepo", () => {
11691169
});
11701170

11711171
describe("repository impact history", () => {
1172+
it("accepts exact SHA-1 and SHA-256 commit identities", () => {
1173+
const sha1 = "a".repeat(40);
1174+
const sha256 = "b".repeat(64);
1175+
const parsed = parseHistoryLog(
1176+
`\x1e${sha1}\x1f1700000000\x1fAlice\0src/a.ts` +
1177+
`\x1e${sha256}\x1f1700000001\x1fBob\0src/b.ts`,
1178+
new Set(["src/a.ts", "src/b.ts"])
1179+
);
1180+
1181+
expect(parsed.commits.map((commit) => commit.hash)).toEqual([sha1, sha256]);
1182+
expect(parsed.inspectedCommits).toBe(2);
1183+
});
1184+
11721185
it("scopes history and file identities to a scanned repository subdirectory", { timeout: HEAVY_GIT_TEST_TIMEOUT }, async () => {
11731186
const root = await mkdtemp(join(tmpdir(), "fixmap-nested-history-"));
11741187
const app = join(root, "app");

0 commit comments

Comments
 (0)