-
Notifications
You must be signed in to change notification settings - Fork 0
Align service deadline checks with tracking window #17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,46 +1,38 @@ | ||
| #!/usr/bin/env node | ||
| // Reads every paid client's Day 0 record and reports how long is left before | ||
| // the internal target and before the refund line. Nothing here mutates state. | ||
| // Reads every paid client's Day 0 record and reports the internal attention | ||
| // status for the Website Correction's 14-day implementation-tracking window. | ||
| // Nothing here mutates state. | ||
| // | ||
| // Two clocks, deliberately: | ||
| // internal target serviceDeadlineAt() — 7 business days, the "7-Day Sprint" promise | ||
| // refund line 14 working days from Day 0, the public delivery guarantee | ||
| // The gap between them is the week of warning before money has to go back. | ||
| // The only contract deadline in the product is the 14-day implementation- | ||
| // tracking window that starts when a human accepts the implementation pass; | ||
| // client delay pauses the clock. The service makes no delivery, revenue, or | ||
| // outcome promise, so this check never reports a customer-facing deadline: | ||
| // clients whose tracking window has not started simply have no deadline | ||
| // applicable. | ||
| // | ||
| // Exit codes: 0 clear · 1 something needs attention today · 2 the check itself broke. | ||
|
|
||
| import {existsSync, readdirSync} from "node:fs" | ||
| import {join} from "node:path" | ||
| import {readJson, resolveRepoPath} from "./lib/service-contract.mjs" | ||
| import {serviceDeadlineAt} from "./lib/service-artifacts.mjs" | ||
| import {addBusinessDaysToTimestamp, businessMillisecondsBetween} from "./date-utils.mjs" | ||
| import {serviceTrackingWindowEndAt} from "./lib/service-artifacts.mjs" | ||
| import {businessMillisecondsBetween} from "./date-utils.mjs" | ||
|
|
||
| const REFUND_BUSINESS_DAYS = 14 | ||
| const WARN_WITHIN_DAYS = 2 | ||
| const DAY_MS = 86400000 | ||
|
|
||
| const repoRoot = process.cwd() | ||
| const asOf = process.env.SERVICE_DEADLINE_NOW || new Date().toISOString() | ||
|
|
||
| // businessMillisecondsBetween refuses a reversed range, so measure the gap in | ||
| // whichever direction is valid and carry the sign ourselves. A past deadline is | ||
| // the whole point of this check — it must not throw. | ||
| // whichever direction is valid and carry the sign ourselves. A past tracking | ||
| // window is the whole point of this check — it must not throw. | ||
| function businessDaysBetween(from, to) { | ||
| const overdue = Date.parse(to) < Date.parse(from) | ||
| const ms = overdue ? -businessMillisecondsBetween(to, from) : businessMillisecondsBetween(from, to) | ||
| return Math.round((ms / DAY_MS) * 10) / 10 | ||
| } | ||
|
|
||
| function refundLineAt(day0StartedAt, pauseHistory) { | ||
| const pausedMs = (pauseHistory || []).reduce( | ||
| (total, pause) => total + businessMillisecondsBetween(pause.startedAt, pause.endedAt), | ||
| 0, | ||
| ) | ||
| const base = addBusinessDaysToTimestamp(day0StartedAt, REFUND_BUSINESS_DAYS) | ||
| // Pauses push the refund line out by the same business time the clock was stopped. | ||
| return new Date(Date.parse(base) + pausedMs).toISOString() | ||
| } | ||
|
|
||
| function loadClients() { | ||
| const dir = resolveRepoPath(repoRoot, "clients") | ||
| if (!existsSync(dir)) return [] | ||
|
|
@@ -49,36 +41,49 @@ function loadClients() { | |
| .map((entry) => { | ||
| const path = join(dir, entry.name, "service-day0.json") | ||
| if (!existsSync(path)) return null | ||
| return {id: entry.name, day0: readJson(path)} | ||
| return {id: entry.name, folder: join(dir, entry.name), day0: readJson(path)} | ||
| }) | ||
| .filter(Boolean) | ||
| } | ||
|
|
||
| function loadState(folder) { | ||
| const path = join(folder, "service-state.json") | ||
| if (!existsSync(path)) return null | ||
| const state = readJson(path) | ||
| return {state: state?.state || "", implementationAcceptedAt: state?.implementationAcceptedAt || ""} | ||
| } | ||
|
|
||
| function assess(client) { | ||
| const {id, day0} = client | ||
| const startedAt = day0.day0StartedAt | ||
| if (!startedAt) return {id, status: "no-day0", detail: "Day 0 not recorded"} | ||
|
|
||
| const paused = Boolean(day0.activePause && day0.activePause.startedAt && !day0.activePause.endedAt) | ||
| const target = day0.deadlineAt || serviceDeadlineAt(startedAt, day0.pauseHistory || []) | ||
| const refund = refundLineAt(startedAt, day0.pauseHistory || []) | ||
|
|
||
| if (paused) { | ||
| return { | ||
| id, paused: true, status: "paused", target, refund, | ||
| id, paused: true, status: "paused", | ||
| detail: `clock paused since ${day0.activePause.startedAt} — ${day0.activePause.reason || "no reason recorded"}`, | ||
| } | ||
| } | ||
|
|
||
| const toTarget = businessDaysBetween(asOf, target) | ||
| const toRefund = businessDaysBetween(asOf, refund) | ||
| const state = loadState(client.folder) | ||
| if (!state) { | ||
| return {id, paused: false, status: "state-missing", detail: "service state not recorded — cannot assess the 14-day tracking window"} | ||
| } | ||
|
|
||
| let status = "on-track" | ||
| if (toRefund <= 0) status = "REFUND-DUE" | ||
| else if (toTarget <= 0) status = "TARGET-MISSED" | ||
| else if (toTarget <= WARN_WITHIN_DAYS) status = "due-soon" | ||
| if (state.state === "tracking-14-day" && state.implementationAcceptedAt) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a tracking review returns Useful? React with 👍 / 👎. |
||
| const windowEnd = serviceTrackingWindowEndAt(state.implementationAcceptedAt, day0.pauseHistory || []) | ||
| const toEnd = businessDaysBetween(asOf, windowEnd) | ||
| let status = "on-track" | ||
| if (toEnd <= 0) status = "TRACKING-OVERDUE" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If implementation is accepted on a weekend, the 14-calendar-day window also ends on a weekend; before that end instant, Useful? React with 👍 / 👎. |
||
| else if (toEnd <= WARN_WITHIN_DAYS) status = "tracking-due-soon" | ||
| return {id, paused: false, status, windowEnd, toEnd} | ||
| } | ||
|
|
||
| return {id, paused: false, status, target, refund, toTarget, toRefund} | ||
| return { | ||
| id, paused: false, status: "tracking-not-started", | ||
| detail: `14-day implementation tracking has not started (current state: ${state.state || "unknown"}); no delivery deadline applies`, | ||
| } | ||
| } | ||
|
|
||
| function main() { | ||
|
|
@@ -88,24 +93,21 @@ function main() { | |
| return 0 | ||
| } | ||
|
|
||
| const rank = {"REFUND-DUE": 0, "TARGET-MISSED": 1, "due-soon": 2, "no-day0": 3, paused: 4, "on-track": 5} | ||
| const rank = {"TRACKING-OVERDUE": 0, "tracking-due-soon": 1, "state-missing": 2, "no-day0": 3, "tracking-not-started": 4, paused: 5, "on-track": 6} | ||
| rows.sort((a, b) => rank[a.status] - rank[b.status]) | ||
|
|
||
| console.log(`TinyStudio service deadlines — as of ${asOf}\n`) | ||
| console.log(`TinyStudio service tracking — as of ${asOf}\n`) | ||
| for (const row of rows) { | ||
| const head = `${row.status.padEnd(14)} ${row.id}` | ||
| if (row.status === "no-day0" || row.paused) { | ||
| const head = `${row.status.padEnd(20)} ${row.id}` | ||
| if (row.status === "no-day0" || row.status === "state-missing" || row.status === "tracking-not-started" || row.paused) { | ||
| console.log(`${head} ${row.detail}`) | ||
| continue | ||
| } | ||
| console.log( | ||
| `${head} ${row.toTarget} business days to internal target, ` + | ||
| `${row.toRefund} to the refund line (${row.refund.slice(0, 10)})`, | ||
| ) | ||
| console.log(`${head} ${row.toEnd} business days to the 14-day tracking-window end (${row.windowEnd.slice(0, 10)})`) | ||
| } | ||
|
|
||
| const needsAttention = rows.filter((row) => | ||
| row.status === "REFUND-DUE" || row.status === "TARGET-MISSED" || row.status === "due-soon" || row.status === "no-day0", | ||
| row.status === "TRACKING-OVERDUE" || row.status === "tracking-due-soon" || row.status === "state-missing" || row.status === "no-day0", | ||
| ) | ||
| if (needsAttention.length > 0) { | ||
| console.log(`\n${needsAttention.length} client(s) need attention today.`) | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -90,6 +90,30 @@ export function serviceDeadlineAt(day0StartedAt, pauseHistory = []) { | |||||||||||||||||||
| const pausedBusinessMs = pauseHistory.reduce((total, pause) => total + businessMillisecondsBetween(pause.startedAt, pause.endedAt), 0) | ||||||||||||||||||||
| return addBusinessMillisecondsToTimestamp(addBusinessDaysToTimestamp(day0StartedAt, 7), pausedBusinessMs) | ||||||||||||||||||||
| } | ||||||||||||||||||||
| const TRACKING_WINDOW_MS = 14 * 86400000 | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // The Website Correction contract tracks 14 active days after human acceptance | ||||||||||||||||||||
| // of the implementation pass; client-delay pauses push the window out by the | ||||||||||||||||||||
| // same wall time the clock was stopped (tracking stage evidence requires at | ||||||||||||||||||||
| // least 14 active days between acceptance and the tracked-through instant). | ||||||||||||||||||||
| // Pauses before acceptance or after the window end do not move it. | ||||||||||||||||||||
| export function serviceTrackingWindowEndAt(implementationAcceptedAt, pauseHistory = []) { | ||||||||||||||||||||
| const anchor = Date.parse(implementationAcceptedAt) | ||||||||||||||||||||
| if (Number.isNaN(anchor)) throw new Error("implementation acceptance timestamp is invalid") | ||||||||||||||||||||
| let end = anchor + TRACKING_WINDOW_MS | ||||||||||||||||||||
| for (let pass = 0; pass <= pauseHistory.length + 2; pass += 1) { | ||||||||||||||||||||
| const pausedWithin = pauseHistory.reduce((total, pause) => { | ||||||||||||||||||||
| const start = Date.parse(pause.startedAt) | ||||||||||||||||||||
| const stop = Date.parse(pause.endedAt) | ||||||||||||||||||||
| if (Number.isNaN(start) || Number.isNaN(stop)) throw new Error("tracking pause interval is invalid") | ||||||||||||||||||||
| return total + Math.max(0, Math.min(end, stop) - Math.max(anchor, start)) | ||||||||||||||||||||
|
Comment on lines
+106
to
+109
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Reject pause intervals that end before they start.
Proposed fix const start = Date.parse(pause.startedAt)
const stop = Date.parse(pause.endedAt)
if (Number.isNaN(start) || Number.isNaN(stop)) throw new Error("tracking pause interval is invalid")
+ if (stop < start) throw new Error("tracking pause interval is invalid")
return total + Math.max(0, Math.min(end, stop) - Math.max(anchor, start))📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||
| }, 0) | ||||||||||||||||||||
| const next = anchor + TRACKING_WINDOW_MS + pausedWithin | ||||||||||||||||||||
| if (next === end) break | ||||||||||||||||||||
| end = next | ||||||||||||||||||||
| } | ||||||||||||||||||||
| return new Date(end).toISOString() | ||||||||||||||||||||
| } | ||||||||||||||||||||
| export function assertExactKeys(value, expected, name) { | ||||||||||||||||||||
| assert(value && typeof value === "object" && !Array.isArray(value), `${name} must be an object`) | ||||||||||||||||||||
| const actual = Object.keys(value).sort() | ||||||||||||||||||||
|
|
@@ -457,7 +481,7 @@ export function validateDay0Record(value, applicationId) { | |||||||||||||||||||
| const {total} = checkedPauseIntervals(value.pauseHistory, value.activePause, Date.parse(value.day0StartedAt), "Day 0") | ||||||||||||||||||||
| assert(Number.isInteger(value.totalPausedMs) && value.totalPausedMs === total, "Day 0 totalPausedMs mismatch") | ||||||||||||||||||||
| const expectedDeadline = serviceDeadlineAt(value.day0StartedAt, value.pauseHistory) | ||||||||||||||||||||
| assert(value.deadlineAt === expectedDeadline, "Day 0 deadline must preserve seven working days and exclude paused time") | ||||||||||||||||||||
| assert(value.deadlineAt === expectedDeadline, "Day 0 deadline must match the recorded deadline and exclude paused time") | ||||||||||||||||||||
| return value | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,6 +27,7 @@ import { | |
| validateStageEvidence | ||
| } from "./lib/review-queue.mjs" | ||
| import {assertCanonicalFounderPilotCohort, assertClientScaffold, FOUNDER_PILOT} from "./lib/client-scaffold.mjs" | ||
| import {serviceTrackingWindowEndAt} from "./lib/service-artifacts.mjs" | ||
| import {createPromotionJournal, promotionMarkerPath, validatePromotionJournal} from "./lib/service-promotion-journal.mjs" | ||
| import {commitJournaledTransition, transitionJournalRecord} from "./lib/service-transition-journal.mjs" | ||
| import {addBusinessDaysToTimestamp, businessMillisecondsBetween, localEndOfIsoDate, localIsoDate, timestampIsOnOrBeforeLocalDate, timestampIsOnOrBeforeTrustedNow, trustedNow} from "./date-utils.mjs" | ||
|
|
@@ -443,6 +444,11 @@ try { | |
| eq(businessMillisecondsBetween("2026-07-11T10:00:00.000Z", "2026-07-12T10:00:00.000Z"), 0) | ||
| eq(serviceDeadlineAt("2026-07-10T10:00:00.000+05:30", [{reason: "Weekend client delay", startedAt: "2026-07-11T10:00:00.000+05:30", endedAt: "2026-07-12T10:00:00.000+05:30", durationMs: 86400000}]), "2026-07-21T10:00:00.000+05:30") | ||
| eq(serviceDeadlineAt("2026-07-16T00:15:00.000+05:30"), "2026-07-27T00:15:00.000+05:30") | ||
| eq(serviceTrackingWindowEndAt("2026-07-13T10:00:00.000Z"), "2026-07-27T10:00:00.000Z") | ||
| eq(serviceTrackingWindowEndAt("2026-07-13T10:00:00.000Z", [{reason: "Client access delay", startedAt: "2026-07-15T10:00:00.000Z", endedAt: "2026-07-17T10:00:00.000Z", durationMs: 172800000}]), "2026-07-29T10:00:00.000Z") | ||
| eq(serviceTrackingWindowEndAt("2026-07-13T10:00:00.000Z", [{reason: "Pre-acceptance delay", startedAt: "2026-07-11T10:00:00.000Z", endedAt: "2026-07-12T10:00:00.000Z", durationMs: 86400000}]), "2026-07-27T10:00:00.000Z") | ||
| eq(serviceTrackingWindowEndAt("2026-07-13T10:00:00.000Z", [{reason: "Straddling delay", startedAt: "2026-07-25T10:00:00.000Z", endedAt: "2026-07-28T10:00:00.000Z", durationMs: 259200000}]), "2026-07-30T10:00:00.000Z") | ||
| thr(() => serviceTrackingWindowEndAt("not-a-date"), /implementation acceptance timestamp is invalid/) | ||
|
Comment on lines
+447
to
+451
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Add coverage for an inverted pause interval. The new tests reject an invalid acceptance timestamp. They do not reject a pause where 🤖 Prompt for AI Agents |
||
|
|
||
| const futureApplication = {...application, applicationId: "018f5a54-84aa-7ae0-a1fd-4da350490779", submittedAt: nextLocalMidnight} | ||
| const futureApplicationPath = rp("future-application.json") | ||
|
|
@@ -2126,6 +2132,81 @@ try { | |
| rm(backupParent, {recursive: true, force: true}) | ||
| } | ||
|
|
||
| // ——— check-service-deadlines.mjs: 14-day tracking attention status ——— | ||
| const deadlineScenarios = [] | ||
| try { | ||
| const scenario = () => { | ||
| const dir = mkdtempSync(join(tmpdir(), "tinystudio-deadline-scenario-")) | ||
| md(join(dir, "clients"), {recursive: true}) | ||
| deadlineScenarios.push(dir) | ||
| return dir | ||
| } | ||
| const runDeadlineCheck = (dir, now) => spawnSync(process.execPath, [join(process.cwd(), "scripts/check-service-deadlines.mjs")], {cwd: dir, env: {...process.env, SERVICE_DEADLINE_NOW: now}, encoding: "utf8"}) | ||
| const day0Fixture = (id, startedAt, pauseHistory = [], activePause = null) => ({ | ||
| applicationId: id, paymentEvidence: `paid: invoice ${id}`, requiredContext: "approved context", | ||
| approvalOwner: "Founder", implementationOwner: "TinyStudio", offerName: "The Website Correction", | ||
| offerPriceUsd: 1000, pricingCohort: "founder-pilot", pilotSequence: 1, ready: true, | ||
| day0StartedAt: startedAt, updatedAt: startedAt, paused: Boolean(activePause), activePause, | ||
| pauseHistory, totalPausedMs: pauseHistory.reduce((total, pause) => total + pause.durationMs, 0), | ||
| deadlineAt: addBusinessDaysToTimestamp(startedAt, 7), resumeState: activePause ? "tracking-14-day" : "" | ||
| }) | ||
| const writeClient = (dir, id, day0, state) => { | ||
| const folder = join(dir, "clients", id) | ||
| md(folder, {recursive: true}) | ||
| aw(join(folder, "service-day0.json"), day0) | ||
| if (state) aw(join(folder, "service-state.json"), state) | ||
| } | ||
| const retiredPromiseLanguage = /\b(?:seven[- ]day|7[- ]day)\b|refund|guarantee|sprint/i | ||
|
|
||
| const trackingDir = scenario() | ||
| const trackingId = "110f5a54-84aa-7ae0-a1fd-4da350490001" | ||
| writeClient(trackingDir, trackingId, day0Fixture(trackingId, "2026-07-01T10:00:00.000Z"), {state: "tracking-14-day", implementationAcceptedAt: "2026-07-13T10:00:00.000Z"}) | ||
| const onTrack = runDeadlineCheck(trackingDir, "2026-07-20T10:00:00.000Z") | ||
| eq(onTrack.status, 0, onTrack.stdout) | ||
| mat(onTrack.stdout, new RegExp(`on-track\\s+${trackingId}\\s+5 business days to the 14-day tracking-window end \\(2026-07-27\\)`)) | ||
| mat(onTrack.stdout, /All clients on track\./) | ||
| const dueSoon = runDeadlineCheck(trackingDir, "2026-07-24T10:00:00.000Z") | ||
| eq(dueSoon.status, 1) | ||
| mat(dueSoon.stdout, new RegExp(`tracking-due-soon\\s+${trackingId}`)) | ||
| const overdue = runDeadlineCheck(trackingDir, "2026-07-28T10:00:00.000Z") | ||
| eq(overdue.status, 1) | ||
| mat(overdue.stdout, new RegExp(`TRACKING-OVERDUE\\s+${trackingId}\\s+-1 business days to the 14-day tracking-window end`)) | ||
|
|
||
| const pausedDir = scenario() | ||
| const pausedId = "220f5a54-84aa-7ae0-a1fd-4da350490002" | ||
| const clientDelay = [{reason: "Client access delay", startedAt: "2026-07-15T10:00:00.000Z", endedAt: "2026-07-17T10:00:00.000Z", durationMs: 172800000}] | ||
| writeClient(pausedDir, pausedId, day0Fixture(pausedId, "2026-07-01T10:00:00.000Z", clientDelay), {state: "tracking-14-day", implementationAcceptedAt: "2026-07-13T10:00:00.000Z"}) | ||
| const extended = runDeadlineCheck(pausedDir, "2026-07-24T10:00:00.000Z") | ||
| eq(extended.status, 0, extended.stdout) | ||
| mat(extended.stdout, new RegExp(`on-track\\s+${pausedId}\\s+3 business days to the 14-day tracking-window end \\(2026-07-29\\)`)) | ||
| eq(runDeadlineCheck(pausedDir, "2026-07-30T10:00:00.000Z").status, 1) | ||
|
|
||
| const mixedDir = scenario() | ||
| const preTrackingId = "330f5a54-84aa-7ae0-a1fd-4da350490003" | ||
| writeClient(mixedDir, preTrackingId, day0Fixture(preTrackingId, "2026-07-01T10:00:00.000Z"), {state: "implementation"}) | ||
| const clockPausedId = "440f5a54-84aa-7ae0-a1fd-4da350490004" | ||
| writeClient(mixedDir, clockPausedId, day0Fixture(clockPausedId, "2026-07-01T10:00:00.000Z", [], {reason: "Client access pending", startedAt: "2026-07-05T10:00:00.000Z"}), {state: "tracking-14-day", implementationAcceptedAt: "2026-07-13T10:00:00.000Z"}) | ||
| const noDay0Id = "550f5a54-84aa-7ae0-a1fd-4da350490005" | ||
| md(join(mixedDir, "clients", noDay0Id), {recursive: true}) | ||
| aw(join(mixedDir, "clients", noDay0Id, "service-day0.json"), {}) | ||
|
Comment on lines
+2189
to
+2191
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Test a missing Day 0 file. This fixture creates Create the client directory without 🤖 Prompt for AI Agents |
||
| const stateMissingId = "660f5a54-84aa-7ae0-a1fd-4da350490006" | ||
| md(join(mixedDir, "clients", stateMissingId), {recursive: true}) | ||
| aw(join(mixedDir, "clients", stateMissingId, "service-day0.json"), day0Fixture(stateMissingId, "2026-07-01T10:00:00.000Z")) | ||
| const mixed = runDeadlineCheck(mixedDir, "2026-07-24T10:00:00.000Z") | ||
| eq(mixed.status, 1) | ||
| mat(mixed.stdout, new RegExp(`tracking-not-started\\s+${preTrackingId}.*14-day implementation tracking has not started \\(current state: implementation\\); no delivery deadline applies`)) | ||
| mat(mixed.stdout, new RegExp(`paused\\s+${clockPausedId}.*clock paused since 2026-07-05T10:00:00.000Z — Client access pending`)) | ||
| mat(mixed.stdout, new RegExp(`no-day0\\s+${noDay0Id}.*Day 0 not recorded`)) | ||
| mat(mixed.stdout, new RegExp(`state-missing\\s+${stateMissingId}.*service state not recorded`)) | ||
| mat(mixed.stdout, /2 client\(s\) need attention today\./) | ||
|
|
||
| for (const output of [onTrack.stdout, dueSoon.stdout, overdue.stdout, extended.stdout, mixed.stdout]) { | ||
| assert(!retiredPromiseLanguage.test(output), "check-service-deadlines output revived retired seven-day/refund language") | ||
| } | ||
| } finally { | ||
| for (const dir of deadlineScenarios) rm(dir, {recursive: true, force: true}) | ||
| } | ||
|
|
||
| assert(ALLOWED_COMMANDS.every(argv => Array.isArray(argv) && argv.every(part => typeof part === "string"))) | ||
| const engineSource = [rf(join(process.cwd(), "scripts/lib/review-queue.mjs"), "utf8"), rf(join(process.cwd(), QUEUE), "utf8")].join("\n") | ||
| assert(!/node:child_process|\bfetch\s*\(|\bexec(?:File)?\s*\(|\bspawn\s*\(/.test(engineSource)) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Retain clients that have no Day 0 file.
When
service-day0.jsonis absent, this code returnsnulland removes the client. The checker then cannot emitno-day0or return exit code1for that client. Keep the directory inloadClientsand use an empty Day 0 value soassessreports the missing record.Proposed fix
.map((entry) => { const path = join(dir, entry.name, "service-day0.json") - if (!existsSync(path)) return null - return {id: entry.name, folder: join(dir, entry.name), day0: readJson(path)} + return { + id: entry.name, + folder: join(dir, entry.name), + day0: existsSync(path) ? readJson(path) : {}, + } }) - .filter(Boolean)📝 Committable suggestion
🤖 Prompt for AI Agents