fix(process): normalize ports that end a sentence

Contextual port normalization refused any port followed by a period, so
common log lines such as "Listening on port: 8080." or
"http://localhost:8080." kept their run-specific port and equivalent
captures compared as changed. Accept a sentence-ending period while
still refusing a period that continues a decimal, version, or host name,
and bump the committed port-normalization version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kaoru0822 2026-10-09 14:06:41 +00:00
parent 33bf585185
commit db98a51dd1
5 changed files with 29 additions and 6 deletions

View File

@ -112,8 +112,10 @@ same admitted PTY chunks rather than a second unbounded stream.
Port normalization recognizes explicit `port`, `tcp_port`, `udp_port`, and
`listen` fields, URL authorities, IP endpoints, and `localhost` endpoints, with
ports from 0 through 65535. It preserves ordinary counters, dimensions, file
line numbers, and ambiguous bare host labels. Use explicit literal `patterns`
ports from 0 through 65535. A port may end a sentence, as in `port: 8080.`;
a period followed by a letter or digit continues a decimal, version, or host
name instead. It preserves ordinary counters, dimensions, file line numbers,
and ambiguous bare host labels. Use explicit literal `patterns`
for a different application-specific spelling. Set `ports: false` to preserve
all endpoint numbers in comparison text. Captures commit the port-normalization
version so older broad numeric normalization cannot silently compare as the

View File

@ -85,7 +85,7 @@ export const processComparisonContract = (
limits: scenario.limits,
normalization: scenario.normalization,
...(scenario.normalization.ports
? { port_normalization_version: "contextual-endpoints-v1" }
? { port_normalization_version: "contextual-endpoints-v2" }
: {}),
});

View File

@ -99,6 +99,25 @@ it("normalizes only contextual ports before overlapping PID values and honors di
).toBe(input);
});
it("normalizes ports that end a sentence but not decimal or host continuations", () => {
const input = [
"Listening on port: 8080.",
"Server at http://localhost:8080. Ready",
"Bound [::1]:3000.",
"listen=9000.\n",
"port: 80.5 localhost:8080.5 localhost:8080.example.test",
].join("\n");
expect(normalizeProcessText(input, scenario({}), "/temporary", rootPid)).toBe(
[
"Listening on port: <port>.",
"Server at http://localhost:<port>. Ready",
"Bound [::1]:<port>.",
"listen=<port>.\n",
"port: 80.5 localhost:8080.5 localhost:8080.example.test",
].join("\n"),
);
});
it("normalizes representative process identities and preserves null, zero, and unrelated numbers", () => {
const before = JSON.stringify(samples);
const normalized = normalizeProcessSamples(samples, scenario({}), rootPid);

View File

@ -41,9 +41,11 @@ export const normalizeProcessText = (
return normalized;
};
// A port may end a sentence, but a period followed by a word character
// continues a decimal, version, or host name and is not a port boundary.
const normalizePortTokens = (value: string): string => {
const endpointNormalized = value.replaceAll(
/(\b[a-z][a-z0-9+.-]*:\/\/[^\s/?#"'<>]+|\blocalhost|\b(?:\d{1,3}\.){3}\d{1,3}|\[[\da-f:.%]+\]):(\d{1,5})(?![\w.@:])/giu,
/(\b[a-z][a-z0-9+.-]*:\/\/[^\s/?#"'<>]+|\blocalhost|\b(?:\d{1,3}\.){3}\d{1,3}|\[[\da-f:.%]+\]):(\d{1,5})(?![\w@:]|\.\w)/giu,
(match: string, endpoint: string, port: string) => {
if (Number(port) > 65_535) return match;
const isEndpoint = endpoint.includes("://")
@ -54,7 +56,7 @@ const normalizePortTokens = (value: string): string => {
},
);
return endpointNormalized.replaceAll(
/(\b(?:port|tcp_port|udp_port|listen)\b["']?\s*[:=]\s*)(\d{1,5})(?![\w.])/giu,
/(\b(?:port|tcp_port|udp_port|listen)\b["']?\s*[:=]\s*)(\d{1,5})(?!\w|\.\w)/giu,
(match: string, prefix: string, port: string) =>
Number(port) <= 65_535 ? `${prefix}<port>` : match,
);

View File

@ -63,7 +63,7 @@ itWithCaptureCapability(
),
).toBe(true);
expect(capture.manifest.comparison_contract).toMatchObject({
port_normalization_version: "contextual-endpoints-v1",
port_normalization_version: "contextual-endpoints-v2",
});
expect(capture.exit).toMatchObject({ code: 0, reason: "exited" });
expect(capture.cleanup).toMatchObject({