litmus-cli 1.4.3 → 1.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/commands/doctor.d.ts +2 -21
  2. package/dist/commands/doctor.d.ts.map +1 -1
  3. package/dist/commands/doctor.js +41 -33
  4. package/dist/commands/doctor.js.map +1 -1
  5. package/dist/commands/submit.d.ts.map +1 -1
  6. package/dist/commands/submit.js +17 -1
  7. package/dist/commands/submit.js.map +1 -1
  8. package/dist/lib/config.d.ts +51 -0
  9. package/dist/lib/config.d.ts.map +1 -1
  10. package/dist/lib/config.js +76 -0
  11. package/dist/lib/config.js.map +1 -1
  12. package/dist/lib/deadline.d.ts +92 -14
  13. package/dist/lib/deadline.d.ts.map +1 -1
  14. package/dist/lib/deadline.js +112 -17
  15. package/dist/lib/deadline.js.map +1 -1
  16. package/dist/lib/detect-project.d.ts.map +1 -1
  17. package/dist/lib/detect-project.js +48 -2
  18. package/dist/lib/detect-project.js.map +1 -1
  19. package/dist/lib/detection.d.ts +32 -0
  20. package/dist/lib/detection.d.ts.map +1 -0
  21. package/dist/lib/detection.js +80 -0
  22. package/dist/lib/detection.js.map +1 -0
  23. package/dist/lib/hook-logger.cjs +50 -2
  24. package/dist/lib/platform.d.ts +34 -10
  25. package/dist/lib/platform.d.ts.map +1 -1
  26. package/dist/lib/platform.js +56 -16
  27. package/dist/lib/platform.js.map +1 -1
  28. package/dist/lib/schemas.d.ts +6 -0
  29. package/dist/lib/schemas.d.ts.map +1 -1
  30. package/dist/lib/schemas.js +8 -0
  31. package/dist/lib/schemas.js.map +1 -1
  32. package/dist/lib/shadow.d.ts +79 -0
  33. package/dist/lib/shadow.d.ts.map +1 -0
  34. package/dist/lib/shadow.js +268 -0
  35. package/dist/lib/shadow.js.map +1 -0
  36. package/dist/lib/tracker.d.ts +1 -1
  37. package/dist/lib/tracker.d.ts.map +1 -1
  38. package/dist/lib/tracker.js +42 -4
  39. package/dist/lib/tracker.js.map +1 -1
  40. package/dist/lib/watcher.js +436 -85
  41. package/dist/lib/watcher.js.map +1 -1
  42. package/dist/lib/zip.d.ts.map +1 -1
  43. package/dist/lib/zip.js +24 -1
  44. package/dist/lib/zip.js.map +1 -1
  45. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  import { readFile, writeFile, mkdir, rename } from "fs/promises";
2
2
  import { existsSync } from "fs";
3
3
  import path from "path";
4
+ import { writeShadowConfig } from "./shadow.js";
4
5
  /**
5
6
  * Calculate the effective deadline timestamp (ms) from config.
6
7
  * Returns null if no deadline or time limit is configured.
@@ -26,6 +27,81 @@ export async function writeConfig(projectRoot, config) {
26
27
  const tmpPath = `${configPath}.litmus-tmp-${process.pid}-${Date.now()}`;
27
28
  await writeFile(tmpPath, JSON.stringify(config, null, 2), "utf8");
28
29
  await rename(tmpPath, configPath);
30
+ // Mirror outside the repo (ENG-1453): the in-repo copy above stays
31
+ // authoritative; the shadow only exists so a deleted config can be
32
+ // restored without the candidate doing anything. Best-effort by design.
33
+ writeShadowConfig(projectRoot, config);
34
+ }
35
+ /**
36
+ * Sanity-check restore-time metadata before writing a rebuilt config.
37
+ * Returns a human-readable problem, or null when the times are usable.
38
+ *
39
+ * The failure this guards against is real: the init route used to answer
40
+ * every re-fetch with `startedAt: now`, and a config rebuilt from that
41
+ * mid-session arms the client-side deadline (startedAt + timeLimit) hours
42
+ * late — the tracker then sleeps through the real cutoff. The route is fixed
43
+ * to return the candidate's true start, and this check refuses any regression
44
+ * of that class rather than writing a config that lies about time. The same
45
+ * gate runs on every automatic restore (watcher self-heal, shadow fallback).
46
+ *
47
+ * A deadline already in the past is deliberately NOT refused: the deadline
48
+ * check reports it, and support flows still need the config on disk.
49
+ */
50
+ export function validateRebuiltTimes(meta, nowMs) {
51
+ const started = Date.parse(meta.startedAt);
52
+ if (isNaN(started))
53
+ return "the start time was unreadable";
54
+ // Small allowance for client clock skew; anything past it means the server
55
+ // stamped a fresh "now" instead of the real start.
56
+ if (started > nowMs + 2 * 60000)
57
+ return "the start time is in the future";
58
+ const effective = getEffectiveDeadline(meta);
59
+ if (effective !== null && effective < started)
60
+ return "the cutoff is earlier than the start time";
61
+ return null;
62
+ }
63
+ /**
64
+ * Fold server-authoritative times into the local config (ENG-1489).
65
+ *
66
+ * The local config is written once at init and the session can outlive its
67
+ * accuracy: extensions, per-candidate overrides, and support corrections all
68
+ * land server-side only. The heartbeat response now carries the current
69
+ * times; this decides whether they change anything locally.
70
+ *
71
+ * Returns:
72
+ * - null when nothing changed (compares INSTANTS, not strings — the
73
+ * backend and the init route format timestamps differently);
74
+ * - { kind: "refused" } when the server payload fails the same sanity gate
75
+ * as rebuilt configs (validateRebuiltTimes) — a server bug stamping a
76
+ * fresh "now" as startedAt must not re-arm a lying clock;
77
+ * - { kind: "changed" } with the updated config otherwise.
78
+ *
79
+ * A null server `startedAt` keeps the local value (the row predates start
80
+ * stamping); null `deadline`/`timeLimit` are meaningful ("no limit") and are
81
+ * applied.
82
+ */
83
+ export function computeTimeSync(current, times, nowMs) {
84
+ const next = {
85
+ ...current,
86
+ startedAt: times.startedAt ?? current.startedAt,
87
+ deadline: times.deadline,
88
+ timeLimit: times.timeLimit,
89
+ };
90
+ const instant = (iso) => {
91
+ if (!iso)
92
+ return null;
93
+ const ms = Date.parse(iso);
94
+ return isNaN(ms) ? null : ms;
95
+ };
96
+ const changed = instant(next.startedAt) !== instant(current.startedAt) ||
97
+ instant(next.deadline) !== instant(current.deadline) ||
98
+ (next.timeLimit ?? null) !== (current.timeLimit ?? null);
99
+ if (!changed)
100
+ return null;
101
+ const refused = validateRebuiltTimes(next, nowMs);
102
+ if (refused)
103
+ return { kind: "refused", reason: refused };
104
+ return { kind: "changed", next };
29
105
  }
30
106
  /**
31
107
  * Walk up from cwd to find the directory containing .litmus/config.json.
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAChE,OAAO,EAAE,UAAU,EAAE,MAAM,IAAI,CAAA;AAC/B,OAAO,IAAI,MAAM,MAAM,CAAA;AACvB,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAuB/C;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAAC,MAAkE;IACrG,MAAM,UAAU,GAAG;QACjB,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,QAAQ;QAChE,MAAM,CAAC,SAAS,IAAI,MAAM,CAAC,SAAS;YAClC,CAAC,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,GAAG,MAAM,CAAC,SAAS,GAAG,KAAM;YAClE,CAAC,CAAC,QAAQ;KACb,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAA;IAE7B,OAAO,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;AAC/D,CAAC;AAED,MAAM,UAAU,GAAG,SAAS,CAAA;AAC5B,MAAM,WAAW,GAAG,aAAa,CAAA;AAEjC,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,WAAmB,EACnB,MAAoB;IAEpB,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,UAAU,CAAC,CAAA;IACpD,MAAM,KAAK,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IAC3C,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,WAAW,CAAC,CAAA;IACpD,4EAA4E;IAC5E,uEAAuE;IACvE,2EAA2E;IAC3E,MAAM,OAAO,GAAG,GAAG,UAAU,eAAe,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE,EAAE,CAAA;IACvE,MAAM,SAAS,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC,CAAA;IACjE,MAAM,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,CAAA;IACjC,mEAAmE;IACnE,mEAAmE;IACnE,wEAAwE;IACxE,iBAAiB,CAAC,WAAW,EAAE,MAAM,CAAC,CAAA;AACxC,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,oBAAoB,CAClC,IAA8E,EAC9E,KAAa;IAEb,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;IAC1C,IAAI,KAAK,CAAC,OAAO,CAAC;QAAE,OAAO,+BAA+B,CAAA;IAC1D,2EAA2E;IAC3E,mDAAmD;IACnD,IAAI,OAAO,GAAG,KAAK,GAAG,CAAC,GAAG,KAAM;QAAE,OAAO,iCAAiC,CAAA;IAC1E,MAAM,SAAS,GAAG,oBAAoB,CAAC,IAAI,CAAC,CAAA;IAC5C,IAAI,SAAS,KAAK,IAAI,IAAI,SAAS,GAAG,OAAO;QAAE,OAAO,2CAA2C,CAAA;IACjG,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,eAAe,CAC7B,OAAqB,EACrB,KAAsF,EACtF,KAAa;IAEb,MAAM,IAAI,GAAiB;QACzB,GAAG,OAAO;QACV,SAAS,EAAE,KAAK,CAAC,SAAS,IAAI,OAAO,CAAC,SAAS;QAC/C,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,SAAS,EAAE,KAAK,CAAC,SAAS;KAC3B,CAAA;IAED,MAAM,OAAO,GAAG,CAAC,GAA8B,EAAiB,EAAE;QAChE,IAAI,CAAC,GAAG;YAAE,OAAO,IAAI,CAAA;QACrB,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;QAC1B,OAAO,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAA;IAC9B,CAAC,CAAA;IACD,MAAM,OAAO,GACX,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC;QACtD,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC;QACpD,CAAC,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,SAAS,IAAI,IAAI,CAAC,CAAA;IAC1D,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAA;IAEzB,MAAM,OAAO,GAAG,oBAAoB,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;IACjD,IAAI,OAAO;QAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,CAAA;IACxD,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,CAAA;AAClC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe;IAC7B,IAAI,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE,CAAA;IACvB,qEAAqE;IACrE,uEAAuE;IACvE,0CAA0C;IAC1C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC;QAC5B,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,UAAU,EAAE,WAAW,CAAC,CAAA;QAC1D,IAAI,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;YAC3B,OAAO,GAAG,CAAA;QACZ,CAAC;QACD,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;QAChC,IAAI,MAAM,KAAK,GAAG;YAAE,MAAK;QACzB,GAAG,GAAG,MAAM,CAAA;IACd,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU;IAC9B,IAAI,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE,CAAA;IAEvB,wEAAwE;IACxE,sEAAsE;IACtE,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC;QAC5B,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,UAAU,EAAE,WAAW,CAAC,CAAA;QAC1D,IAAI,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;YAC3B,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC,CAAA;YAC9C,IAAI,CAAC;gBACH,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAiB,CAAA;YACxC,CAAC;YAAC,MAAM,CAAC;gBACP,MAAM,IAAI,KAAK,CACb,0GAA0G,CAC3G,CAAA;YACH,CAAC;QACH,CAAC;QAED,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;QAChC,IAAI,MAAM,KAAK,GAAG;YAAE,MAAK,CAAC,0BAA0B;QACpD,GAAG,GAAG,MAAM,CAAA;IACd,CAAC;IAED,OAAO,IAAI,CAAA;AACb,CAAC"}
@@ -9,29 +9,107 @@
9
9
  * wake: the interval also pauses while suspended, but the first tick after
10
10
  * resume sees that wall-clock has passed the deadline and fires immediately.
11
11
  * Polling additionally sidesteps the ~24.8-day `setTimeout` ceiling.
12
+ *
13
+ * Two pieces (ENG-1489 / ENG-1490):
14
+ *
15
+ * - `armDeadlinePoll` — the dumb wall-clock poll. It reads the CURRENT
16
+ * deadline through a getter on every tick, because the deadline can move
17
+ * mid-session (server-side extensions and corrections arrive via the
18
+ * heartbeat response). It never latches and never clears itself on fire —
19
+ * dedupe, retries, and the fire decision live in the controller.
20
+ *
21
+ * - `createAutoSubmitController` — the fire decision. It never submits on
22
+ * local state alone: every attempt re-verifies the deadline against the
23
+ * server first. A stale-early local deadline that fires "successfully" is
24
+ * strictly worse than one that fails — the server, which believes the
25
+ * deadline is still hours or days away, accepts the upload as an ordinary
26
+ * early submission and the candidate's session ends prematurely. The
27
+ * verification is what makes retrying safe at all.
12
28
  */
13
29
  export interface DeadlinePollHandle {
14
30
  stop(): void;
15
31
  }
16
- export interface ArmDeadlineOptions {
17
- /** Absolute deadline as an epoch-ms timestamp. */
18
- deadline: number;
32
+ /** Tick gaps beyond this are reported as a suspend/resume (wake) even when the
33
+ * poll cadence is long. */
34
+ export declare const WAKE_GAP_MIN_MS: number;
35
+ export interface DeadlinePollOptions {
36
+ /** Read the current effective deadline (epoch ms), or null when none is
37
+ * configured. Re-read every tick so mid-session changes take effect. */
38
+ getDeadline: () => number | null;
19
39
  /** Poll cadence in ms. */
20
40
  pollMs: number;
21
- /** Fired exactly once, on the first tick at/after the deadline. */
22
- onFire: () => void;
41
+ /** Called on EVERY tick at/after the deadline. The caller owns dedupe. */
42
+ onDue: () => void;
23
43
  /** Injectable clock (defaults to Date.now) — used by tests. */
24
44
  now?: () => number;
25
- /** Called when the poll is armed, with ms until the deadline. */
26
- onArmed?: (msUntilDeadline: number) => void;
27
- /** Called instead of arming when the deadline is already in the past. */
28
- onAlreadyPassed?: () => void;
45
+ /** Called once when the poll is armed, with ms until the deadline (null when
46
+ * no deadline is configured yet). */
47
+ onArmed?: (msUntilDeadline: number | null) => void;
48
+ /** Called when consecutive ticks are separated by far more than pollMs — the
49
+ * process was suspended (lid close / sleep) and just resumed. */
50
+ onWake?: (gapMs: number) => void;
29
51
  }
30
52
  /**
31
- * Arm a poll that fires `onFire` once when `now() >= deadline`.
32
- * Returns a handle to stop the poll, or `null` if the deadline had already
33
- * passed at arm time (in which case `onAlreadyPassed` is called and nothing is
34
- * scheduled the server-side deadline path owns that case).
53
+ * Arm a poll that calls `onDue` on every tick where `now() >= getDeadline()`.
54
+ * Always arms, even when the deadline is currently null or already past: a
55
+ * null deadline can become real via server sync, and a watcher restarted
56
+ * after the deadline should still hand the (server-verified) recovery
57
+ * decision to the controller instead of silently skipping.
35
58
  */
36
- export declare function armDeadlineAutoSubmit(opts: ArmDeadlineOptions): DeadlinePollHandle | null;
59
+ export declare function armDeadlinePoll(opts: DeadlinePollOptions): DeadlinePollHandle;
60
+ /** Outcome of asking the server for the authoritative times. */
61
+ export type VerifyTimesResult =
62
+ /** Server answered; times were synced by the caller. `effectiveDeadline` is
63
+ * the post-sync effective deadline (epoch ms) or null. */
64
+ {
65
+ status: "ok";
66
+ effectiveDeadline: number | null;
67
+ }
68
+ /** Network error / 5xx — transient; try again shortly. */
69
+ | {
70
+ status: "unavailable";
71
+ }
72
+ /** 4xx — the token was refused (session likely over). Do not submit; the
73
+ * activity-upload path independently shuts the watcher down on
74
+ * "Session over". */
75
+ | {
76
+ status: "rejected";
77
+ };
78
+ export interface AutoSubmitOutcome {
79
+ kind: "submitted" | "failed" | "averted" | "deferred" | "exhausted";
80
+ /** 1-based attempt number for submitted/failed. */
81
+ attempt?: number;
82
+ detail?: string;
83
+ }
84
+ export interface AutoSubmitControllerOptions {
85
+ /** Fetch authoritative times from the server AND apply them locally
86
+ * (config + poll getter). Must not throw. */
87
+ verifyTimes: () => Promise<VerifyTimesResult>;
88
+ /** Run `litmus submit --yes`. Must not throw. */
89
+ runSubmit: () => Promise<{
90
+ ok: boolean;
91
+ error?: string;
92
+ }>;
93
+ onOutcome?: (outcome: AutoSubmitOutcome) => void;
94
+ now?: () => number;
95
+ /** Verified-due submit attempts before giving up (server cron owns the
96
+ * session after that). */
97
+ maxAttempts?: number;
98
+ /** Backoff after a failed submit attempt; last entry repeats. */
99
+ retryBackoffMs?: number[];
100
+ /** Re-check cadence while the server is unreachable at the deadline. */
101
+ verifyRetryMs?: number;
102
+ }
103
+ export interface AutoSubmitController {
104
+ /** Called by the poll on every due tick. Serialized internally. */
105
+ handleDue(): Promise<void>;
106
+ /** Called when a server sync moved the deadline. A deadline now in the
107
+ * future resets the attempt budget — the earlier failures belonged to a
108
+ * cutoff that no longer exists. */
109
+ noteDeadlineChanged(newDeadlineMs: number | null): void;
110
+ /** External signal that the session is over (e.g. upload path saw
111
+ * "Session over") — stop attempting. */
112
+ markDone(): void;
113
+ }
114
+ export declare function createAutoSubmitController(opts: AutoSubmitControllerOptions): AutoSubmitController;
37
115
  //# sourceMappingURL=deadline.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"deadline.d.ts","sourceRoot":"","sources":["../../src/lib/deadline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,WAAW,kBAAkB;IACjC,IAAI,IAAI,IAAI,CAAA;CACb;AAED,MAAM,WAAW,kBAAkB;IACjC,kDAAkD;IAClD,QAAQ,EAAE,MAAM,CAAA;IAChB,0BAA0B;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,mEAAmE;IACnE,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,+DAA+D;IAC/D,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;IAClB,iEAAiE;IACjE,OAAO,CAAC,EAAE,CAAC,eAAe,EAAE,MAAM,KAAK,IAAI,CAAA;IAC3C,yEAAyE;IACzE,eAAe,CAAC,EAAE,MAAM,IAAI,CAAA;CAC7B;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,kBAAkB,GAAG,kBAAkB,GAAG,IAAI,CAuBzF"}
1
+ {"version":3,"file":"deadline.d.ts","sourceRoot":"","sources":["../../src/lib/deadline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,MAAM,WAAW,kBAAkB;IACjC,IAAI,IAAI,IAAI,CAAA;CACb;AAED;2BAC2B;AAC3B,eAAO,MAAM,eAAe,QAAa,CAAA;AAEzC,MAAM,WAAW,mBAAmB;IAClC;4EACwE;IACxE,WAAW,EAAE,MAAM,MAAM,GAAG,IAAI,CAAA;IAChC,0BAA0B;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,0EAA0E;IAC1E,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,+DAA+D;IAC/D,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;IAClB;yCACqC;IACrC,OAAO,CAAC,EAAE,CAAC,eAAe,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IAClD;qEACiE;IACjE,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;CACjC;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,mBAAmB,GAAG,kBAAkB,CAuB7E;AAED,gEAAgE;AAChE,MAAM,MAAM,iBAAiB;AAC3B;0DAC0D;AACxD;IAAE,MAAM,EAAE,IAAI,CAAC;IAAC,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE;AACpD,0DAA0D;GACxD;IAAE,MAAM,EAAE,aAAa,CAAA;CAAE;AAC3B;;qBAEqB;GACnB;IAAE,MAAM,EAAE,UAAU,CAAA;CAAE,CAAA;AAE1B,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,WAAW,GAAG,QAAQ,GAAG,SAAS,GAAG,UAAU,GAAG,WAAW,CAAA;IACnE,mDAAmD;IACnD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,2BAA2B;IAC1C;iDAC6C;IAC7C,WAAW,EAAE,MAAM,OAAO,CAAC,iBAAiB,CAAC,CAAA;IAC7C,iDAAiD;IACjD,SAAS,EAAE,MAAM,OAAO,CAAC;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACzD,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,IAAI,CAAA;IAChD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;IAClB;8BAC0B;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,iEAAiE;IACjE,cAAc,CAAC,EAAE,MAAM,EAAE,CAAA;IACzB,wEAAwE;IACxE,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,MAAM,WAAW,oBAAoB;IACnC,mEAAmE;IACnE,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IAC1B;;uCAEmC;IACnC,mBAAmB,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAAA;IACvD;4CACwC;IACxC,QAAQ,IAAI,IAAI,CAAA;CACjB;AAED,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,2BAA2B,GAAG,oBAAoB,CA6ElG"}
@@ -9,32 +9,127 @@
9
9
  * wake: the interval also pauses while suspended, but the first tick after
10
10
  * resume sees that wall-clock has passed the deadline and fires immediately.
11
11
  * Polling additionally sidesteps the ~24.8-day `setTimeout` ceiling.
12
+ *
13
+ * Two pieces (ENG-1489 / ENG-1490):
14
+ *
15
+ * - `armDeadlinePoll` — the dumb wall-clock poll. It reads the CURRENT
16
+ * deadline through a getter on every tick, because the deadline can move
17
+ * mid-session (server-side extensions and corrections arrive via the
18
+ * heartbeat response). It never latches and never clears itself on fire —
19
+ * dedupe, retries, and the fire decision live in the controller.
20
+ *
21
+ * - `createAutoSubmitController` — the fire decision. It never submits on
22
+ * local state alone: every attempt re-verifies the deadline against the
23
+ * server first. A stale-early local deadline that fires "successfully" is
24
+ * strictly worse than one that fails — the server, which believes the
25
+ * deadline is still hours or days away, accepts the upload as an ordinary
26
+ * early submission and the candidate's session ends prematurely. The
27
+ * verification is what makes retrying safe at all.
12
28
  */
29
+ /** Tick gaps beyond this are reported as a suspend/resume (wake) even when the
30
+ * poll cadence is long. */
31
+ export const WAKE_GAP_MIN_MS = 2 * 60000;
13
32
  /**
14
- * Arm a poll that fires `onFire` once when `now() >= deadline`.
15
- * Returns a handle to stop the poll, or `null` if the deadline had already
16
- * passed at arm time (in which case `onAlreadyPassed` is called and nothing is
17
- * scheduled the server-side deadline path owns that case).
33
+ * Arm a poll that calls `onDue` on every tick where `now() >= getDeadline()`.
34
+ * Always arms, even when the deadline is currently null or already past: a
35
+ * null deadline can become real via server sync, and a watcher restarted
36
+ * after the deadline should still hand the (server-verified) recovery
37
+ * decision to the controller instead of silently skipping.
18
38
  */
19
- export function armDeadlineAutoSubmit(opts) {
39
+ export function armDeadlinePoll(opts) {
20
40
  const now = opts.now ?? Date.now;
21
- if (now() >= opts.deadline) {
22
- opts.onAlreadyPassed?.();
23
- return null;
24
- }
25
- opts.onArmed?.(opts.deadline - now());
26
- let fired = false;
41
+ const initial = opts.getDeadline();
42
+ opts.onArmed?.(initial === null ? null : initial - now());
43
+ let lastTick = now();
27
44
  const timer = setInterval(() => {
28
- if (fired)
29
- return;
30
- if (now() >= opts.deadline) {
31
- fired = true;
32
- clearInterval(timer);
33
- opts.onFire();
45
+ const t = now();
46
+ const gap = t - lastTick;
47
+ lastTick = t;
48
+ if (gap > Math.max(opts.pollMs * 4, WAKE_GAP_MIN_MS)) {
49
+ opts.onWake?.(gap);
50
+ }
51
+ const deadline = opts.getDeadline();
52
+ if (deadline !== null && t >= deadline) {
53
+ opts.onDue();
34
54
  }
35
55
  }, opts.pollMs);
36
56
  // Intentionally NOT unref()'d: the watcher should stay alive to fire the
37
57
  // auto-submit even if every other keep-alive source went quiet.
38
58
  return { stop: () => clearInterval(timer) };
39
59
  }
60
+ export function createAutoSubmitController(opts) {
61
+ const now = opts.now ?? Date.now;
62
+ const maxAttempts = opts.maxAttempts ?? 3;
63
+ const backoffs = opts.retryBackoffMs ?? [60000, 5 * 60000];
64
+ const verifyRetryMs = opts.verifyRetryMs ?? 2 * 60000;
65
+ let state = "idle";
66
+ let attempts = 0;
67
+ let gateUntil = 0;
68
+ let exhaustedReported = false;
69
+ async function handleDue() {
70
+ if (state !== "idle")
71
+ return;
72
+ if (now() < gateUntil)
73
+ return;
74
+ if (attempts >= maxAttempts) {
75
+ if (!exhaustedReported) {
76
+ exhaustedReported = true;
77
+ opts.onOutcome?.({ kind: "exhausted", attempt: attempts });
78
+ }
79
+ return;
80
+ }
81
+ state = "in_flight";
82
+ try {
83
+ const verdict = await opts.verifyTimes();
84
+ if (verdict.status === "unavailable") {
85
+ // Can't verify ⇒ can't safely submit (and an upload would almost
86
+ // certainly fail on the same dead network). Doesn't burn an attempt:
87
+ // a machine that wakes offline must still submit when the network
88
+ // returns, however long that takes.
89
+ gateUntil = now() + verifyRetryMs;
90
+ opts.onOutcome?.({ kind: "deferred", detail: "server unreachable for deadline verification" });
91
+ return;
92
+ }
93
+ if (verdict.status === "rejected") {
94
+ gateUntil = now() + verifyRetryMs * 5;
95
+ opts.onOutcome?.({ kind: "deferred", detail: "server rejected deadline verification" });
96
+ return;
97
+ }
98
+ if (verdict.effectiveDeadline === null || now() < verdict.effectiveDeadline) {
99
+ // The local deadline was stale-early — the sync inside verifyTimes
100
+ // already re-armed the poll with the server's value. This is the
101
+ // protective path: without it, a submit here would have been
102
+ // ACCEPTED by the server as an ordinary early submission.
103
+ opts.onOutcome?.({ kind: "averted" });
104
+ return;
105
+ }
106
+ attempts += 1;
107
+ const res = await opts.runSubmit();
108
+ if (res.ok) {
109
+ state = "done";
110
+ opts.onOutcome?.({ kind: "submitted", attempt: attempts });
111
+ return;
112
+ }
113
+ gateUntil = now() + backoffs[Math.min(attempts - 1, backoffs.length - 1)];
114
+ opts.onOutcome?.({ kind: "failed", attempt: attempts, detail: res.error });
115
+ }
116
+ finally {
117
+ if (state === "in_flight")
118
+ state = "idle";
119
+ }
120
+ }
121
+ function noteDeadlineChanged(newDeadlineMs) {
122
+ if (state === "done")
123
+ return;
124
+ if (newDeadlineMs !== null && newDeadlineMs > now()) {
125
+ attempts = 0;
126
+ gateUntil = 0;
127
+ exhaustedReported = false;
128
+ }
129
+ }
130
+ function markDone() {
131
+ state = "done";
132
+ }
133
+ return { handleDue, noteDeadlineChanged, markDone };
134
+ }
40
135
  //# sourceMappingURL=deadline.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"deadline.js","sourceRoot":"","sources":["../../src/lib/deadline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAqBH;;;;;GAKG;AACH,MAAM,UAAU,qBAAqB,CAAC,IAAwB;IAC5D,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAA;IAEhC,IAAI,GAAG,EAAE,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;QAC3B,IAAI,CAAC,eAAe,EAAE,EAAE,CAAA;QACxB,OAAO,IAAI,CAAA;IACb,CAAC;IAED,IAAI,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,QAAQ,GAAG,GAAG,EAAE,CAAC,CAAA;IAErC,IAAI,KAAK,GAAG,KAAK,CAAA;IACjB,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE;QAC7B,IAAI,KAAK;YAAE,OAAM;QACjB,IAAI,GAAG,EAAE,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC3B,KAAK,GAAG,IAAI,CAAA;YACZ,aAAa,CAAC,KAAK,CAAC,CAAA;YACpB,IAAI,CAAC,MAAM,EAAE,CAAA;QACf,CAAC;IACH,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,CAAA;IACf,yEAAyE;IACzE,gEAAgE;IAEhE,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,aAAa,CAAC,KAAK,CAAC,EAAE,CAAA;AAC7C,CAAC"}
1
+ {"version":3,"file":"deadline.js","sourceRoot":"","sources":["../../src/lib/deadline.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"detect-project.d.ts","sourceRoot":"","sources":["../../src/lib/detect-project.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,WAAW,GACnB,MAAM,GACN,QAAQ,GACR,IAAI,GACJ,MAAM,GACN,QAAQ,GACR,MAAM,GACN,MAAM,GACN,KAAK,GACL,SAAS,CAAA;AAEb,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,WAAW,CAAA;IACjB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,sBAAsB,SAAS,CAAA;AA0D5C;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,IAAI;IAC1C,OAAO,EAAE,MAAM,CAAA;IACf,aAAa,EAAE,OAAO,CAAA;IACtB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;CAC/B,CAkBA;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,CAwEtD"}
1
+ {"version":3,"file":"detect-project.d.ts","sourceRoot":"","sources":["../../src/lib/detect-project.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,WAAW,GACnB,MAAM,GACN,QAAQ,GACR,IAAI,GACJ,MAAM,GACN,QAAQ,GACR,MAAM,GACN,MAAM,GACN,KAAK,GACL,SAAS,CAAA;AAEb,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,WAAW,CAAA;IACjB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,sBAAsB,SAAS,CAAA;AA0D5C;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,IAAI;IAC1C,OAAO,EAAE,MAAM,CAAA;IACf,aAAa,EAAE,OAAO,CAAA;IACtB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;CAC/B,CAkBA;AAyBD;;GAEG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,CAmGtD"}
@@ -92,6 +92,27 @@ export function resolvePythonInterpreter() {
92
92
  const last = candidates[candidates.length - 1];
93
93
  return { command: last.command, matchesGrader: false, detectedVersion: null };
94
94
  }
95
+ /**
96
+ * Is `bin` runnable from PATH?
97
+ *
98
+ * `--version` rather than `which`/`where`: one probe that works on all three
99
+ * platforms, and it proves the binary actually EXECUTES. A PATH entry pointing at
100
+ * a broken install or the wrong architecture resolves fine and then fails on use,
101
+ * which is the case a warning most needs to catch. `shell: true` on Windows so
102
+ * `.cmd`/`.ps1` shims resolve, matching the Python probe above.
103
+ */
104
+ function isOnPath(bin) {
105
+ try {
106
+ return (spawnSync(bin, ["--version"], {
107
+ encoding: "utf8",
108
+ shell: process.platform === "win32",
109
+ }).status === 0);
110
+ }
111
+ catch {
112
+ // spawnSync throws (rather than returning a status) on some ENOENT paths.
113
+ return false;
114
+ }
115
+ }
95
116
  /**
96
117
  * Detect project type and suggest an install command.
97
118
  */
@@ -99,12 +120,37 @@ export function detectProject(dir) {
99
120
  if (existsSync(path.join(dir, "package.json"))) {
100
121
  const hasYarnLock = existsSync(path.join(dir, "yarn.lock"));
101
122
  const hasPnpmLock = existsSync(path.join(dir, "pnpm-lock.yaml"));
123
+ // A bun lockfile has to steer this, not just be tolerated. `npm install` on a
124
+ // Bun project does populate node_modules, but it ignores bun.lock and writes a
125
+ // COMPETING package-lock.json next to it — so the candidate ends up with a tree
126
+ // resolved fresh from the caret ranges, which is the drift the lockfile exists
127
+ // to prevent, plus a stray lockfile in their submission.
128
+ const hasBunLock = existsSync(path.join(dir, "bun.lock")) || existsSync(path.join(dir, "bun.lockb"));
102
129
  const installCommand = hasPnpmLock
103
130
  ? "pnpm install"
104
131
  : hasYarnLock
105
132
  ? "yarn"
106
- : "npm install";
107
- return { type: "node", installCommand };
133
+ : hasBunLock
134
+ ? "bun install"
135
+ : "npm install";
136
+ // Same disclosure contract as the Python skew below: a missing toolchain is
137
+ // ours to state here, not theirs to infer. Without this, `init`'s install
138
+ // failure prints "run `bun install` manually", which is the command that just
139
+ // failed — and the candidate is left guessing that a runtime they may never
140
+ // have heard of is the missing piece.
141
+ //
142
+ // Gated on the SELECTED command, not on `hasBunLock`. A repo carrying both a
143
+ // bun lockfile and a pnpm/yarn one resolves to pnpm/yarn above, and telling
144
+ // that candidate to install Bun and run `bun install` would send them to
145
+ // install a second, competing dependency tree over the one we just built.
146
+ // Also means npm/yarn/pnpm repos never pay for the PATH probe.
147
+ const toolchainWarning = installCommand === "bun install" && !isOnPath("bun")
148
+ ? "This assessment's scaffold is a Bun project (bun.lock), and Bun isn't on " +
149
+ "your PATH. Install it from https://bun.sh, then re-run `bun install` in " +
150
+ "this folder. npm can install the dependencies but cannot run the " +
151
+ "scaffold's server — it uses Bun-only APIs."
152
+ : undefined;
153
+ return { type: "node", installCommand, toolchainWarning };
108
154
  }
109
155
  if (existsSync(path.join(dir, "requirements.txt")) ||
110
156
  existsSync(path.join(dir, "pyproject.toml")) ||
@@ -1 +1 @@
1
- {"version":3,"file":"detect-project.js","sourceRoot":"","sources":["../../src/lib/detect-project.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AACzC,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,IAAI,CAAA;AAC5C,OAAO,IAAI,MAAM,MAAM,CAAA;AAyBvB;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,MAAM,CAAA;AAS5C;;;;;;;;;;;;;;;;GAgBG;AACH,SAAS,gBAAgB;IACvB,MAAM,OAAO,GAAG,sBAAsB,CAAA;IACtC,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QACjC,OAAO;YACL,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,IAAI,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,OAAO,OAAO,EAAE,EAAE;YAC/D,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE;YACtC,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE;YAC9C,EAAE,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE;SACjD,CAAA;IACH,CAAC;IACD,OAAO;QACL,EAAE,GAAG,EAAE,SAAS,OAAO,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,OAAO,EAAE,SAAS,OAAO,EAAE,EAAE;QAClE,EAAE,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE;KACjD,CAAA;AACH,CAAC;AAED,2EAA2E;AAC3E,SAAS,YAAY,CAAC,SAA0B;IAC9C,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,SAAS,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC,GAAG,SAAS,CAAC,IAAI,EAAE,WAAW,CAAC,EAAE;YACxE,QAAQ,EAAE,MAAM;YAChB,KAAK,EAAE,OAAO,CAAC,QAAQ,KAAK,OAAO;SACpC,CAAC,CAAA;QACF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAA;QACpC,MAAM,KAAK,GAAG,6BAA6B,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,MAAM,IAAI,EAAE,GAAG,MAAM,CAAC,MAAM,IAAI,EAAE,EAAE,CAAC,CAAA;QAChG,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;IAChC,CAAC;IAAC,MAAM,CAAC;QACP,iEAAiE;QACjE,OAAO,IAAI,CAAA;IACb,CAAC;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,wBAAwB;IAKtC,MAAM,UAAU,GAAG,gBAAgB,EAAE,CAAA;IACrC,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,MAAM,eAAe,GAAG,YAAY,CAAC,SAAS,CAAC,CAAA;QAC/C,IAAI,CAAC,eAAe;YAAE,SAAQ;QAC9B,OAAO;YACL,OAAO,EAAE,SAAS,CAAC,OAAO;YAC1B,wEAAwE;YACxE,mEAAmE;YACnE,sCAAsC;YACtC,aAAa,EAAE,eAAe,CAAC,UAAU,CAAC,GAAG,sBAAsB,GAAG,CAAC;gBACrE,eAAe,KAAK,sBAAsB;YAC5C,eAAe;SAChB,CAAA;IACH,CAAC;IAED,MAAM,IAAI,GAAG,UAAU,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;IAC9C,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,eAAe,EAAE,IAAI,EAAE,CAAA;AAC/E,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,aAAa,CAAC,GAAW;IACvC,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC,EAAE,CAAC;QAC/C,MAAM,WAAW,GAAG,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC,CAAA;QAC3D,MAAM,WAAW,GAAG,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,gBAAgB,CAAC,CAAC,CAAA;QAChE,MAAM,cAAc,GAAG,WAAW;YAChC,CAAC,CAAC,cAAc;YAChB,CAAC,CAAC,WAAW;gBACX,CAAC,CAAC,MAAM;gBACR,CAAC,CAAC,aAAa,CAAA;QACnB,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,CAAA;IACzC,CAAC;IAED,IACE,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,kBAAkB,CAAC,CAAC;QAC9C,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,gBAAgB,CAAC,CAAC;QAC5C,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC,EACtC,CAAC;QACD,0EAA0E;QAC1E,0EAA0E;QAC1E,MAAM,MAAM,GAAG,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,cAAc,CAAA;QACnF,MAAM,MAAM,GAAG,wBAAwB,EAAE,CAAA;QACzC,MAAM,cAAc,GAAG,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,kBAAkB,CAAC,CAAC;YACnE,CAAC,CAAC,GAAG,MAAM,CAAC,OAAO,oBAAoB,MAAM,8BAA8B;YAC3E,CAAC,CAAC,GAAG,MAAM,CAAC,OAAO,oBAAoB,MAAM,eAAe,CAAA;QAC9D,2EAA2E;QAC3E,yEAAyE;QACzE,yEAAyE;QACzE,4EAA4E;QAC5E,mDAAmD;QACnD,MAAM,gBAAgB,GAAG,MAAM,CAAC,aAAa;YAC3C,CAAC,CAAC,SAAS;YACX,CAAC,CAAC,MAAM,CAAC,eAAe;gBACtB,CAAC,CAAC,uCAAuC,sBAAsB,yBAAyB;oBACtF,UAAU,MAAM,CAAC,eAAe,OAAO,MAAM,CAAC,OAAO,wCAAwC;oBAC7F,oEAAoE;oBACpE,GAAG,sBAAsB,oDAAoD;gBAC/E,CAAC,CAAC,sDAAsD,MAAM,CAAC,OAAO,kBAAkB;oBACtF,sDAAsD,sBAAsB,aAAa;oBACzF,0CAA0C,CAAA;QAChD,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,cAAc,EAAE,gBAAgB,EAAE,CAAA;IAC7D,CAAC;IAED,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC,EAAE,CAAC;QACzC,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,cAAc,EAAE,iBAAiB,EAAE,CAAA;IAC1D,CAAC;IAED,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC,EAAE,CAAC;QAC1C,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,aAAa,EAAE,CAAA;IACxD,CAAC;IAED,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,kBAAkB,CAAC,CAAC,EAAE,CAAC;QACjG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,cAAc,EAAE,CAAA;IACzD,CAAC;IAED,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,CAAC,CAAA;IAC9B,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC;QACjE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,cAAc,EAAE,gBAAgB,EAAE,CAAA;IAC7D,CAAC;IAED,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC,EAAE,CAAC;QAC7C,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,IAAI,EAAE,CAAA;IAC/C,CAAC;IAED,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC,EAAE,CAAC;QAC1C,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,gBAAgB,EAAE,CAAA;IAC3D,CAAC;IAED,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC;QAChD,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAA;IAC5D,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,cAAc,EAAE,IAAI,EAAE,CAAA;AAClD,CAAC"}
1
+ {"version":3,"file":"detect-project.js","sourceRoot":"","sources":["../../src/lib/detect-project.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Claude surface classification — pure functions, unit-testable (watcher.ts
3
+ * is a spawned script, so logic living there can't be imported by tests).
4
+ *
5
+ * "claude" on a candidate machine is at least three different products, and
6
+ * the old bare-word pattern labeled them all `claude` — which report readers
7
+ * took to mean Claude Code with zero prompts captured, i.e. broken tracking:
8
+ * claude_code — the CLI / IDE extension; prompts capturable via hook
9
+ * claude_desktop — the Desktop app; no hook exists
10
+ * claude_web — claude.ai in a browser tab; no hook can exist
11
+ * claude — legacy/unattributable, kept so old readers keep working
12
+ */
13
+ /**
14
+ * Classifies every process-list line containing "claude" into surfaces.
15
+ * `ps` is the lowercased `ps aux` / `tasklist` output from getProcessList().
16
+ * A line matching no surface pattern stays legacy `claude` (unattributable —
17
+ * notably Windows tasklist, where Desktop and Claude Code are both bare
18
+ * `claude.exe` image names, and bystander strings like the VS Code
19
+ * extension's install path in some process's args).
20
+ */
21
+ export declare function classifyClaudeProcesses(ps: string): string[];
22
+ /**
23
+ * Re-labels an Anthropic-bound connection by its owning process. The IP alone
24
+ * cannot separate the surfaces (claude.ai is Cloudflare-fronted and shares
25
+ * IPs), but the owner can, and each surface reads very differently in a
26
+ * report: Claude Code captures prompts via hook; Claude Desktop and a browser
27
+ * tab cannot capture anything.
28
+ *
29
+ * `owner` is the process name exactly as the OS reported it — case matters.
30
+ */
31
+ export declare function refineClaudeTool(tool: string, owner: string): string;
32
+ //# sourceMappingURL=detection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detection.d.ts","sourceRoot":"","sources":["../../src/lib/detection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,EAAE,CAmB5D;AAQD;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAepE"}
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Claude surface classification — pure functions, unit-testable (watcher.ts
3
+ * is a spawned script, so logic living there can't be imported by tests).
4
+ *
5
+ * "claude" on a candidate machine is at least three different products, and
6
+ * the old bare-word pattern labeled them all `claude` — which report readers
7
+ * took to mean Claude Code with zero prompts captured, i.e. broken tracking:
8
+ * claude_code — the CLI / IDE extension; prompts capturable via hook
9
+ * claude_desktop — the Desktop app; no hook exists
10
+ * claude_web — claude.ai in a browser tab; no hook can exist
11
+ * claude — legacy/unattributable, kept so old readers keep working
12
+ */
13
+ /**
14
+ * Classifies every process-list line containing "claude" into surfaces.
15
+ * `ps` is the lowercased `ps aux` / `tasklist` output from getProcessList().
16
+ * A line matching no surface pattern stays legacy `claude` (unattributable —
17
+ * notably Windows tasklist, where Desktop and Claude Code are both bare
18
+ * `claude.exe` image names, and bystander strings like the VS Code
19
+ * extension's install path in some process's args).
20
+ */
21
+ export function classifyClaudeProcesses(ps) {
22
+ const found = new Set();
23
+ for (const line of ps.split("\n")) {
24
+ if (!/\bclaude\b/.test(line))
25
+ continue;
26
+ if (/claude\.app|claude helper/.test(line)) {
27
+ // macOS Desktop app bundle and its renderer/GPU helpers
28
+ found.add("claude_desktop");
29
+ }
30
+ else if (/claude-code[/\\]cli\.js|[/\\]claude(?:\s|$)/.test(line)) {
31
+ // The npm entrypoint, or the `claude` binary itself as the executed
32
+ // path. Path-anchored so the word inside a directory name (the VS Code
33
+ // extension's install path, a .claude config dir) doesn't count as the
34
+ // CLI running. Desktop's binary also ends in /claude but its lines
35
+ // match the app-bundle branch above first.
36
+ found.add("claude_code");
37
+ }
38
+ else {
39
+ found.add("claude");
40
+ }
41
+ }
42
+ return [...found];
43
+ }
44
+ // Owners that mean "a browser tab". First-token matching: lsof reports
45
+ // multi-word app names ("Google Chrome Helper", "Brave Browser") but the
46
+ // leading token identifies the browser; Safari's network traffic comes from
47
+ // com.apple.WebKit.Networking.
48
+ const BROWSER_OWNER_RE = /^(google|chrome|chromium|safari|com\.apple\.webkit|firefox|msedge|microsoft|edge|brave|arc|opera|vivaldi)/i;
49
+ /**
50
+ * Re-labels an Anthropic-bound connection by its owning process. The IP alone
51
+ * cannot separate the surfaces (claude.ai is Cloudflare-fronted and shares
52
+ * IPs), but the owner can, and each surface reads very differently in a
53
+ * report: Claude Code captures prompts via hook; Claude Desktop and a browser
54
+ * tab cannot capture anything.
55
+ *
56
+ * `owner` is the process name exactly as the OS reported it — case matters.
57
+ */
58
+ export function refineClaudeTool(tool, owner) {
59
+ if (tool !== "claude_web" && tool !== "claude_api")
60
+ return tool;
61
+ if (!owner)
62
+ return tool; // OS gave no owner — keep the IP-derived label
63
+ // Windows image name: Desktop and Claude Code are both `claude.exe`, so the
64
+ // surface stays unattributable.
65
+ if (/^claude\.exe$/i.test(owner))
66
+ return "claude";
67
+ // Case is the signal on mac/linux: `claude` is the Claude Code CLI binary,
68
+ // `Claude` / `Claude Helper …` is the Desktop app (verified live on macOS).
69
+ if (/^claude(-code)?$/.test(owner))
70
+ return "claude_code";
71
+ if (/^Claude\b/.test(owner))
72
+ return "claude_desktop";
73
+ if (BROWSER_OWNER_RE.test(owner))
74
+ return tool;
75
+ // Some other app owns the socket (an Electron webview, a shared Cloudflare
76
+ // IP that isn't Anthropic at all). claude.ai traffic keeps the vendor but
77
+ // drops the surface claim; api traffic keeps its API label.
78
+ return tool === "claude_web" ? "claude" : tool;
79
+ }
80
+ //# sourceMappingURL=detection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detection.js","sourceRoot":"","sources":["../../src/lib/detection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH;;;;;;;GAOG;AACH,MAAM,UAAU,uBAAuB,CAAC,EAAU;IAChD,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAA;IAC/B,KAAK,MAAM,IAAI,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QAClC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,SAAQ;QACtC,IAAI,2BAA2B,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAC3C,wDAAwD;YACxD,KAAK,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAA;QAC7B,CAAC;aAAM,IAAI,6CAA6C,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YACpE,oEAAoE;YACpE,uEAAuE;YACvE,uEAAuE;YACvE,mEAAmE;YACnE,2CAA2C;YAC3C,KAAK,CAAC,GAAG,CAAC,aAAa,CAAC,CAAA;QAC1B,CAAC;aAAM,CAAC;YACN,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;QACrB,CAAC;IACH,CAAC;IACD,OAAO,CAAC,GAAG,KAAK,CAAC,CAAA;AACnB,CAAC;AAED,uEAAuE;AACvE,yEAAyE;AACzE,4EAA4E;AAC5E,+BAA+B;AAC/B,MAAM,gBAAgB,GAAG,4GAA4G,CAAA;AAErI;;;;;;;;GAQG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY,EAAE,KAAa;IAC1D,IAAI,IAAI,KAAK,YAAY,IAAI,IAAI,KAAK,YAAY;QAAE,OAAO,IAAI,CAAA;IAC/D,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAA,CAAC,+CAA+C;IACvE,4EAA4E;IAC5E,gCAAgC;IAChC,IAAI,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,QAAQ,CAAA;IACjD,2EAA2E;IAC3E,4EAA4E;IAC5E,IAAI,kBAAkB,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,aAAa,CAAA;IACxD,IAAI,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,gBAAgB,CAAA;IACpD,IAAI,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IAC7C,2EAA2E;IAC3E,0EAA0E;IAC1E,4DAA4D;IAC5D,OAAO,IAAI,KAAK,YAAY,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAA;AAChD,CAAC"}