Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 44 additions & 42 deletions scripts/check-service-deadlines.mjs
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 []
Expand All @@ -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)
Comment on lines 42 to 46

Copy link
Copy Markdown

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.json is absent, this code returns null and removes the client. The checker then cannot emit no-day0 or return exit code 1 for that client. Keep the directory in loadClients and use an empty Day 0 value so assess reports 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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)
const path = join(dir, entry.name, "service-day0.json")
return {
id: entry.name,
folder: join(dir, entry.name),
day0: existsSync(path) ? readJson(path) : {},
}
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/check-service-deadlines.mjs` around lines 42 - 46, Update loadClients
so a missing service-day0.json does not return null or get removed by
filter(Boolean); retain the client entry with an empty Day 0 value, allowing
assess to emit no-day0 and return exit code 1.

}

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) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep checking tracking while awaiting information

When a tracking review returns needs-info, transitionFor creates the valid state {state: "needs-info", resumeState: "tracking-14-day"} while preserving implementationAcceptedAt; this interval is not excluded from active tracking unless a separate Day 0 pause is recorded. This condition therefore falls through to tracking-not-started, and an overdue client can produce exit code 0 until the information request is resolved. Treat a needs-info state whose resumeState is tracking-14-day as an active tracking window.

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"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Compare the wall-clock deadline before marking overdue

If implementation is accepted on a weekend, the 14-calendar-day window also ends on a weekend; before that end instant, businessMillisecondsBetween returns zero because the remaining interval contains no business time. For example, acceptance at Saturday 2026-07-18 10:00Z and checking at 09:00Z on Saturday 2026-08-01 produces toEnd === 0, so this line reports TRACKING-OVERDUE one hour early. Determine overdue from the actual timestamps and use business-day distance only for the warning/display calculation.

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() {
Expand All @@ -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.`)
Expand Down
26 changes: 25 additions & 1 deletion scripts/lib/service-artifacts.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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.

Date.parse accepts both timestamps when endedAt precedes startedAt. The Math.max expression then treats the invalid interval as zero duration. The deadline checker consumes this helper directly, so malformed persisted data can report an earlier tracking-window end.

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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))
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))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/lib/service-artifacts.mjs` around lines 106 - 109, Update the
pause-interval validation in the helper containing the start/stop Date.parse
logic to reject intervals where stop is earlier than start, in addition to
invalid timestamps. Throw the existing invalid-interval error before calculating
the Math.max/Math.min duration, while preserving valid interval handling.

}, 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()
Expand Down Expand Up @@ -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
}

Expand Down
81 changes: 81 additions & 0 deletions scripts/test-service-engine.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 endedAt is before startedAt. Add this assertion with the validation fix so the helper cannot silently exclude malformed pauses.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/test-service-engine.mjs` around lines 447 - 451, Add a
serviceTrackingWindowEndAt test assertion for a pause whose endedAt precedes
startedAt, expecting the helper to reject it with the appropriate validation
error. Update the helper’s pause-interval validation so inverted intervals throw
rather than being silently excluded, while preserving valid pause handling.


const futureApplication = {...application, applicationId: "018f5a54-84aa-7ae0-a1fd-4da350490779", submittedAt: nextLocalMidnight}
const futureApplicationPath = rp("future-application.json")
Expand Down Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 service-day0.json with {}. It does not test the missing-record case. loadClients currently skips a client directory when that file is absent, so the checker does not report the client or return attention status.

Create the client directory without service-day0.json after updating loadClients to retain that client.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/test-service-engine.mjs` around lines 2189 - 2191, Update loadClients
to retain client directories even when service-day0.json is absent, allowing the
checker to report the client and return attention status. In the noDay0Id
fixture, create only the client directory and remove the aw call that creates
service-day0.json with an empty object, so the test exercises the missing-file
case.

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))
Expand Down