diff --git a/docs/plans/state.json b/docs/plans/state.json index b56872a..2e5f429 100644 --- a/docs/plans/state.json +++ b/docs/plans/state.json @@ -439,6 +439,18 @@ ], "test_status": "pnpm test:run src/audit/phase0BaselineScriptWiring.test.ts + pnpm typecheck passing" }, + "phase0-live-baseline-rolling-tag-validation-hardening": { + "status": "completed", + "date": "2026-02-27", + "updated": "2026-02-27", + "summary": "Hardened rolling artifact retention tag parsing to reject impossible timestamp components (month/day/time bounds and invalid calendar dates) so malformed filenames cannot be misclassified through date normalization.", + "files_modified": [ + "src/audit/phase0BaselineArtifactRetention.ts", + "src/audit/phase0BaselineArtifactRetention.test.ts", + "docs/plans/state.json" + ], + "test_status": "pnpm test:run src/audit/phase0BaselineArtifactRetention.test.ts + pnpm typecheck passing" + }, "phase0-instrumentation-ticket-checklist": { "status": "completed", "date": "2026-02-25", diff --git a/src/audit/phase0BaselineArtifactRetention.test.ts b/src/audit/phase0BaselineArtifactRetention.test.ts index 9764fb0..fd34309 100644 --- a/src/audit/phase0BaselineArtifactRetention.test.ts +++ b/src/audit/phase0BaselineArtifactRetention.test.ts @@ -97,4 +97,19 @@ describe('phase0BaselineArtifactRetention', () => { it('rejects negative keep limit', () => { expect(() => planRollingPhase0ArtifactRetention([], -1)).toThrow('keepPerFamily'); }); + + it('ignores malformed rolling tags with impossible date or time values', () => { + const rows = collectRollingPhase0ArtifactFiles([ + 'phase0_baseline_live_2026-13-27-010203.json', + 'phase0_baseline_live_gateway_2026-02-30-010203.json', + 'phase0_baseline_live_backend_pi_embedded_2026-02-27-246001.json', + 'phase0_baseline_live_backend_native_2026-02-27-016061.json', + 'phase0_baseline_live_backend_drift_2026-02-27-010203.md', + 'phase0_baseline_live_prune_2026-00-27-010203.json', + ]); + + expect(rows).toHaveLength(1); + expect(rows[0]?.file_name).toBe('phase0_baseline_live_backend_drift_2026-02-27-010203.md'); + expect(rows[0]?.family).toBe('backend_drift'); + }); }); diff --git a/src/audit/phase0BaselineArtifactRetention.ts b/src/audit/phase0BaselineArtifactRetention.ts index ffc5002..177165e 100644 --- a/src/audit/phase0BaselineArtifactRetention.ts +++ b/src/audit/phase0BaselineArtifactRetention.ts @@ -74,9 +74,28 @@ function parseRollingTagTimestampMs(tag: string): number | undefined { if (!Number.isFinite(hour) || !Number.isFinite(minute) || !Number.isFinite(second)) { return undefined; } + if (month < 1 || month > 12 || day < 1 || day > 31 || hour < 0 || hour > 23 || minute < 0 || minute > 59 || second < 0 || second > 59) { + return undefined; + } const timestampMs = Date.UTC(year, month - 1, day, hour, minute, second); - return Number.isFinite(timestampMs) ? timestampMs : undefined; + if (!Number.isFinite(timestampMs)) { + return undefined; + } + + const utc = new Date(timestampMs); + if ( + utc.getUTCFullYear() !== year + || utc.getUTCMonth() !== month - 1 + || utc.getUTCDate() !== day + || utc.getUTCHours() !== hour + || utc.getUTCMinutes() !== minute + || utc.getUTCSeconds() !== second + ) { + return undefined; + } + + return timestampMs; } function parseRollingArtifactFile(fileName: string): Phase0RollingArtifactFile | undefined {