@company-semantics/contracts 99.0.0 → 99.1.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@company-semantics/contracts",
3
- "version": "99.0.0",
3
+ "version": "99.1.0",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
package/src/index.ts CHANGED
@@ -324,7 +324,10 @@ export * from "./product-intake/index";
324
324
  // for one target document; the wire carries facts and a count, never content.
325
325
  // See ./source-sync/vocabulary.ts and ./source-sync/schemas.ts.
326
326
  export * from "./source-sync/index";
327
-
328
327
  // Quality control plane vocabulary (PRD-01088 / quality-control-plane)
329
328
  // Closed evaluation/decision sets plus QualityRunReportSchema — ids, scores and failure kinds only. See ./quality/index.ts.
330
329
  export * from "./quality/index";
330
+ // Scheduled-execution vocabulary (PRD-01175 / schedule-execution-wire-types)
331
+ // Schedule definitions, slot-derived execution identity, lifecycle, the pure
332
+ // UTC cron evaluator and the schedule route responses. See ./schedules/README.md.
333
+ export * from "./schedules";
@@ -0,0 +1,118 @@
1
+ # schedules/
2
+
3
+ The shared vocabulary of scheduled execution: what a schedule promises, how one
4
+ execution of it is named, the states it moves through, and the UTC cron
5
+ evaluator both control and the backend use to decide when a slot is due.
6
+
7
+ ## Purpose
8
+
9
+ Name, time and track every scheduled job across the fleet with one vocabulary,
10
+ so control (which authors the registry) and the backend (which stores the
11
+ ledger and reconciles) can never disagree about a slot.
12
+
13
+ ## Invariants
14
+
15
+ - An execution is named by its INTENDED slot: `execution_id = <schedule_id>/<YYYY-MM-DDTHH:MM:00Z>`;
16
+ a dispatch attempt is `<execution_id>#<attempt>` and is the workflow's `run-name`.
17
+ A run without one never satisfies a slot.
18
+ - `parseCron` is the only cron evaluator: five fields, UTC, Vixie day matching,
19
+ no names / `L` / `W` / `#`; an expression that can never fire does not parse.
20
+ - Every ScheduleDefinition declares `effects`; `fenced` and `unsafe` require
21
+ `idempotency: "required"`; `catchUp: "all"` never replays `unsafe` effects.
22
+ - `canonicalScheduleRegistryJson` is the only serialization the snapshot's
23
+ `contentHash` is computed over.
24
+ - Response schemas only; request bodies live in the backend beside their routes
25
+ (ScheduleTickRequest is a type, not a schema).
26
+ - Pure: no clock reads, no I/O, only `zod`.
27
+
28
+ ## Dependencies
29
+
30
+ `zod` only. Reached through the package root; there is no `./schedules` subpath.
31
+
32
+ <!-- BEGIN GENERATED: readme-public-api — derived from code by `pnpm readme-api`. Do not edit. -->
33
+
34
+ ## Public API
35
+
36
+ - `CronField` _(type)_ — Five-field UTC cron evaluator for the schedule registry.
37
+ - `CronParseResult` _(type)_
38
+ - `ParsedCron` _(type)_
39
+ - `ParsedDisplayTitle` _(type)_
40
+ - `ParsedExecutionId` _(type)_
41
+ - `SCHEDULE_CATCH_UP_POLICIES`
42
+ - `SCHEDULE_CLAIM_REFUSALS`
43
+ - `SCHEDULE_CLOCK_SOURCES` — Independent tick sources.
44
+ - `SCHEDULE_DEFINITION_KEYS` — Key order used by canonicalScheduleRegistryJson.
45
+ - `SCHEDULE_DISPATCH_INPUTS` — The workflowdispatch inputs every registered workflow declares once it leaves mode `off`.
46
+ - `SCHEDULE_DISPATCH_OUTCOMES`
47
+ - `SCHEDULE_EFFECTS` — What the workflow's side effects tolerate.
48
+ - `SCHEDULE_EXECUTION_STATES` — Scheduled-execution lifecycle, recovery and incident vocabulary.
49
+ - `SCHEDULE_EXECUTION_TRANSITIONS` — Allowed moves.
50
+ - `SCHEDULE_FINISH_REJECTIONS`
51
+ - `SCHEDULE_IDEMPOTENCY_LEVELS`
52
+ - `SCHEDULE_ID_MAX_LENGTH`
53
+ - `SCHEDULE_ID_PATTERN` — Kebab-case, no `/` or `#`, so an executionid and a display title parse unambiguously.
54
+ - `SCHEDULE_INCIDENT_CLASSES`
55
+ - `SCHEDULE_INFRA_LABEL`
56
+ - `SCHEDULE_LOSS_ACK_LABEL`
57
+ - `SCHEDULE_MAX_CATCH_UP_SLOTS`
58
+ - `SCHEDULE_MAX_TIMEOUT_MINUTES` — GitHub-hosted job ceiling.
59
+ - `SCHEDULE_METRICS_NAMESPACE` — CloudWatch EMF metric names (backend emits; control's watchdog script alarms).
60
+ - `SCHEDULE_METRIC_ENVIRONMENT_DIMENSION` — Every Schedules metric carries this dimension (value: the backend deploy environment, e.g. "production" /…
61
+ - `SCHEDULE_METRIC_RECONCILE_SUCCESS`
62
+ - `SCHEDULE_METRIC_TICK`
63
+ - `SCHEDULE_METRIC_TICK_SOURCE_DIMENSION`
64
+ - `SCHEDULE_MODES` — off: GitHub cron only, clock ignores it. shadow: clock plans, never dispatches. live: clock dispatches…
65
+ - `SCHEDULE_OUTPUT_VERIFIERS` — Closed catalog. v0 has one verifier.
66
+ - `SCHEDULE_REGISTRY_SCHEMA_VERSION`
67
+ - `SCHEDULE_RUN_CONCLUSIONS`
68
+ - `SCHEDULE_RUN_NAME` — The exact `run-name:` a registered workflow declares, so displaytitle = executionid#attempt.
69
+ - `SCHEDULE_SEVERITIES`
70
+ - `SCHEDULE_SLOT_ISO_PATTERN`
71
+ - `SCHEDULE_TICK_PATH` — The tick route.
72
+ - `SCHEDULE_TIMEZONE`
73
+ - `ScheduleCatchUpPolicy` _(type)_
74
+ - `ScheduleClaimRefusal` _(type)_
75
+ - `ScheduleClaimResponse` _(type)_
76
+ - `ScheduleClaimResponseSchema`
77
+ - `ScheduleClockSource` _(type)_
78
+ - `ScheduleDefinition` _(type)_
79
+ - `ScheduleDefinitionSchema`
80
+ - `ScheduleDispatchInput` _(type)_
81
+ - `ScheduleDispatchOutcome` _(type)_
82
+ - `ScheduleEffects` _(type)_
83
+ - `ScheduleExecutionSettlementInput` _(type)_
84
+ - `ScheduleExecutionState` _(type)_
85
+ - `ScheduleFinishRejection` _(type)_
86
+ - `ScheduleFinishResponse` _(type)_
87
+ - `ScheduleFinishResponseSchema`
88
+ - `ScheduleIdempotency` _(type)_
89
+ - `ScheduleIncidentClass` _(type)_
90
+ - `ScheduleMode` _(type)_
91
+ - `ScheduleOutputVerifier` _(type)_
92
+ - `ScheduleRegistrySnapshot` _(type)_
93
+ - `ScheduleRegistrySnapshotSchema`
94
+ - `ScheduleRunConclusion` _(type)_
95
+ - `ScheduleSeverity` _(type)_
96
+ - `ScheduleTickRequest` _(type)_ — Tick request body, as a TYPE only: request schemas stay in the backend beside the route (contracts…
97
+ - `ScheduleTickResponse` _(type)_
98
+ - `ScheduleTickResponseSchema` — One tick's outcome, reported to the tick source that triggered it.
99
+ - `SlotWindow` _(type)_
100
+ - `canTransitionScheduleExecution`
101
+ - `canonicalScheduleRegistryJson` — Deterministic serialization: definitions sorted by id, keys in SCHEDULEDEFINITIONKEYS order, no whitespace.
102
+ - `cronMatches` — True when `instant` is exactly a slot of `cron` (whole minute, UTC).
103
+ - `displayTitleFor`
104
+ - `executionIdFor`
105
+ - `isScheduleExecutionSettled` — Settled = nothing further will happen to this slot without a human.
106
+ - `isScheduleId`
107
+ - `maxIntervalMinutes` — The longest gap, in minutes, between consecutive slots among the `samples` intervals ending at or before…
108
+ - `mayReexecuteAutomatically` — none: read-only. idempotent: downstream writes keyed by executionid. fenced: downstream writes carry…
109
+ - `nextSlot` — The earliest slot strictly after `after`, or null when none exists in the search window.
110
+ - `parseCron` — Parse a five-field UTC cron expression.
111
+ - `parseDisplayTitle` — Inverse of displayTitleFor; null for any run title that is not a scheduled dispatch.
112
+ - `parseExecutionId`
113
+ - `previousSlot` — The latest slot at or before `atOrBefore`, or null when none exists in the search window.
114
+ - `scheduleIncidentKey` — Dedupe key for an incident.
115
+ - `slotIso` — `YYYY-MM-DDTHH:MM:00Z`.
116
+ - `slotsBetween` — Slots in (afterExclusive, atOrBefore, newest `limit` kept, returned ascending.
117
+
118
+ <!-- END GENERATED: readme-public-api -->
@@ -0,0 +1,27 @@
1
+ # schedules/\_\_tests\_\_/
2
+
3
+ ## Purpose
4
+
5
+ Locks the scheduled-execution vocabulary's behavior with table tests.
6
+
7
+ - `cron.test.ts` — locks the UTC cron evaluator: every cron the fleet's
8
+ workflows carry today parses (the `IN_USE` table, measured 2026-10-10);
9
+ unsupported syntax (names, `L`, `W`, `#`, `?`, macros, a step on a single
10
+ value) and expressions that can never fire are rejected; day-of-week 7 folds
11
+ into Sunday; Vixie day matching (OR only when both day fields are restricted);
12
+ previous/next slots across month ends and leap days; truncated slot windows
13
+ keep the newest slots; and the longest interval of an irregular schedule.
14
+
15
+ ## Invariants
16
+
17
+ - Every instant is passed in explicitly; no test reads the clock.
18
+ - The `IN_USE` table is a measurement of the fleet's workflows. A new cron in a
19
+ workflow belongs here too, so the evaluator is proven against it.
20
+
21
+ ## Public API
22
+
23
+ None — test-only.
24
+
25
+ ## Dependencies
26
+
27
+ `vitest` and `../cron`.
@@ -0,0 +1,72 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ SCHEDULE_METRICS_NAMESPACE,
4
+ SCHEDULE_METRIC_RECONCILE_SUCCESS,
5
+ SCHEDULE_METRIC_TICK,
6
+ SCHEDULE_METRIC_TICK_SOURCE_DIMENSION,
7
+ SCHEDULE_METRIC_ENVIRONMENT_DIMENSION,
8
+ SCHEDULE_TICK_PATH,
9
+ ScheduleClaimResponseSchema,
10
+ ScheduleFinishResponseSchema,
11
+ ScheduleTickResponseSchema,
12
+ } from "../api";
13
+
14
+ describe("schedules api responses", () => {
15
+ it("admits a legacy run without an execution id", () => {
16
+ expect(
17
+ ScheduleClaimResponseSchema.safeParse({
18
+ outcome: "legacy",
19
+ scheduleId: "pin-audit",
20
+ mode: "shadow",
21
+ }).success,
22
+ ).toBe(true);
23
+ expect(
24
+ ScheduleClaimResponseSchema.safeParse({
25
+ outcome: "legacy",
26
+ scheduleId: "pin-audit",
27
+ mode: "shadow",
28
+ executionId: "x",
29
+ }).success,
30
+ ).toBe(false);
31
+ expect(
32
+ ScheduleClaimResponseSchema.safeParse({
33
+ outcome: "refused",
34
+ reason: "legacy_run_not_admitted",
35
+ }).success,
36
+ ).toBe(true);
37
+ expect(
38
+ ScheduleFinishResponseSchema.safeParse({
39
+ recorded: false,
40
+ reason: "stale_epoch",
41
+ }).success,
42
+ ).toBe(true);
43
+ expect(
44
+ ScheduleTickResponseSchema.safeParse({
45
+ source: "edge",
46
+ tickedAt: "2026-10-10T10:00:00Z",
47
+ reconcileOk: true,
48
+ registryVersion: null,
49
+ dispatched: 0,
50
+ incidentsOpened: 0,
51
+ incidentsResolved: 0,
52
+ }).success,
53
+ ).toBe(true);
54
+ });
55
+
56
+ it("keeps the tick path and metric names stable", () => {
57
+ expect(SCHEDULE_TICK_PATH).toBe("/api/schedules/tick");
58
+ expect([
59
+ SCHEDULE_METRICS_NAMESPACE,
60
+ SCHEDULE_METRIC_TICK,
61
+ SCHEDULE_METRIC_TICK_SOURCE_DIMENSION,
62
+ SCHEDULE_METRIC_RECONCILE_SUCCESS,
63
+ SCHEDULE_METRIC_ENVIRONMENT_DIMENSION,
64
+ ]).toEqual([
65
+ "Schedules",
66
+ "Tick",
67
+ "source",
68
+ "ReconcileSuccess",
69
+ "environment",
70
+ ]);
71
+ });
72
+ });
@@ -0,0 +1,140 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ maxIntervalMinutes,
4
+ nextSlot,
5
+ parseCron,
6
+ previousSlot,
7
+ slotsBetween,
8
+ type ParsedCron,
9
+ } from "../cron";
10
+
11
+ function cron(expression: string): ParsedCron {
12
+ const result = parseCron(expression);
13
+ if (!result.ok) throw new Error(`${expression}: ${result.reason}`);
14
+ return result.cron;
15
+ }
16
+
17
+ const at = (iso: string) => new Date(iso);
18
+ const iso = (d: Date | null) => (d === null ? null : d.toISOString());
19
+
20
+ /** Every distinct cron on the seven Node repos' origin/main workflows, measured 2026-10-10. */
21
+ const IN_USE = [
22
+ "*/30 * * * *",
23
+ "7 * * * *",
24
+ "13,43 * * * *",
25
+ "17 * * * *",
26
+ "30 */6 * * *",
27
+ "0 6 * * *",
28
+ "0 7 * * *",
29
+ "0 8 * * *",
30
+ "0 10 * * *",
31
+ "0 14 * * *",
32
+ "0 15 * * *",
33
+ "17 16 * * *",
34
+ "17 9 * * 1",
35
+ "30 9 * * 1,4",
36
+ "0 2 * * *",
37
+ "0 3 * * *",
38
+ "0 4 * * *",
39
+ "20 4 * * *",
40
+ "30 4 * * *",
41
+ "40 4 * * *",
42
+ "17 5 * * *",
43
+ "50 5 * * *",
44
+ ];
45
+
46
+ describe("schedules cron", () => {
47
+ it("parses every cron the registry carries today", () => {
48
+ for (const expression of IN_USE)
49
+ expect(parseCron(expression).ok).toBe(true);
50
+ });
51
+
52
+ it("rejects names, L/W/#, a step on a single value and impossible dates", () => {
53
+ for (const expression of [
54
+ "0 0 * JAN *",
55
+ "0 0 * * MON",
56
+ "0 0 L * *",
57
+ "0 0 1W * *",
58
+ "0 0 * * 1#2",
59
+ "0 0 ? * *",
60
+ "@hourly",
61
+ "5/15 * * * *",
62
+ "60 * * * *",
63
+ "* * *",
64
+ "0 0 31 2 *",
65
+ ]) {
66
+ expect(parseCron(expression).ok).toBe(false);
67
+ }
68
+ });
69
+
70
+ it("folds day-of-week 7 into Sunday", () => {
71
+ expect(iso(nextSlot(cron("0 0 * * 7"), at("2026-10-10T13:25:00Z")))).toBe(
72
+ "2026-10-11T00:00:00.000Z",
73
+ );
74
+ });
75
+
76
+ it("applies Vixie OR when both day fields are restricted", () => {
77
+ const c = cron("0 0 1 * 0");
78
+ // Sunday 2026-10-11 matches day-of-week even though it is not the 1st.
79
+ expect(iso(nextSlot(c, at("2026-10-10T13:25:00Z")))).toBe(
80
+ "2026-10-11T00:00:00.000Z",
81
+ );
82
+ });
83
+
84
+ it("treats a star-step day-of-month as unrestricted (Vixie)", () => {
85
+ // */2 counts as unrestricted, so both must match: odd day AND Sunday.
86
+ const c = cron("0 0 */2 * 0");
87
+ expect(iso(nextSlot(c, at("2026-10-12T00:00:00Z")))).toBe(
88
+ "2026-10-25T00:00:00.000Z",
89
+ );
90
+ });
91
+
92
+ it("finds the previous and next slot across month ends and leap days", () => {
93
+ expect(iso(nextSlot(cron("0 0 31 * *"), at("2026-10-31T00:00:00Z")))).toBe(
94
+ "2026-12-31T00:00:00.000Z",
95
+ );
96
+ const leap = cron("0 0 29 2 *");
97
+ expect(iso(previousSlot(leap, at("2026-10-10T00:00:00Z")))).toBe(
98
+ "2024-02-29T00:00:00.000Z",
99
+ );
100
+ expect(iso(nextSlot(leap, at("2026-10-10T00:00:00Z")))).toBe(
101
+ "2028-02-29T00:00:00.000Z",
102
+ );
103
+ expect(
104
+ iso(previousSlot(cron("*/30 * * * *"), at("2026-10-10T13:00:00Z"))),
105
+ ).toBe("2026-10-10T13:00:00.000Z");
106
+ });
107
+
108
+ it("keeps the newest slots when a window is truncated", () => {
109
+ const window = slotsBetween(
110
+ cron("*/5 * * * *"),
111
+ at("2026-10-10T12:00:00Z"),
112
+ at("2026-10-10T13:00:00Z"),
113
+ 4,
114
+ );
115
+ expect(window.truncated).toBe(true);
116
+ expect(window.slots.map(iso)).toEqual([
117
+ "2026-10-10T12:45:00.000Z",
118
+ "2026-10-10T12:50:00.000Z",
119
+ "2026-10-10T12:55:00.000Z",
120
+ "2026-10-10T13:00:00.000Z",
121
+ ]);
122
+ const exact = slotsBetween(
123
+ cron("*/30 * * * *"),
124
+ at("2026-10-10T12:00:00Z"),
125
+ at("2026-10-10T13:00:00Z"),
126
+ 10,
127
+ );
128
+ expect(exact).toEqual({
129
+ slots: [at("2026-10-10T12:30:00Z"), at("2026-10-10T13:00:00Z")],
130
+ truncated: false,
131
+ });
132
+ });
133
+
134
+ it("measures the longest interval of an irregular schedule", () => {
135
+ const now = at("2026-10-10T13:25:52Z");
136
+ expect(maxIntervalMinutes(cron("13,43 * * * *"), now)).toBe(30);
137
+ expect(maxIntervalMinutes(cron("30 9 * * 1,4"), now)).toBe(4 * 24 * 60);
138
+ expect(maxIntervalMinutes(cron("17 9 * * 1"), now)).toBe(7 * 24 * 60);
139
+ });
140
+ });
@@ -0,0 +1,97 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ ScheduleDefinitionSchema,
4
+ ScheduleRegistrySnapshotSchema,
5
+ canonicalScheduleRegistryJson,
6
+ type ScheduleDefinition,
7
+ } from "../definition";
8
+
9
+ const PRODUCTION_SIGNALS: ScheduleDefinition = {
10
+ id: "production-signals-signals",
11
+ repo: "company-semantics-control",
12
+ workflow: "production-signals.yml",
13
+ cron: "*/30 * * * *",
14
+ timezone: "UTC",
15
+ graceMinutes: 5,
16
+ timeoutMinutes: 15,
17
+ admissionTimeoutMinutes: 10,
18
+ catchUp: "latest",
19
+ maxCatchUpSlots: 4,
20
+ idempotency: "required",
21
+ effects: "idempotent",
22
+ verify: "production_signal_fresh",
23
+ mode: "off",
24
+ severity: "red-main",
25
+ };
26
+
27
+ describe("schedules definition", () => {
28
+ it("accepts the production-signals definition", () => {
29
+ expect(ScheduleDefinitionSchema.safeParse(PRODUCTION_SIGNALS).success).toBe(
30
+ true,
31
+ );
32
+ });
33
+
34
+ it("requires idempotency required for fenced or unsafe effects", () => {
35
+ for (const effects of ["fenced", "unsafe"] as const) {
36
+ const result = ScheduleDefinitionSchema.safeParse({
37
+ ...PRODUCTION_SIGNALS,
38
+ effects,
39
+ idempotency: "best_effort",
40
+ });
41
+ expect(result.success).toBe(false);
42
+ }
43
+ const replayUnsafe = ScheduleDefinitionSchema.safeParse({
44
+ ...PRODUCTION_SIGNALS,
45
+ effects: "unsafe",
46
+ catchUp: "all",
47
+ });
48
+ expect(replayUnsafe.success).toBe(false);
49
+ });
50
+
51
+ it("rejects an unknown field", () => {
52
+ expect(
53
+ ScheduleDefinitionSchema.safeParse({ ...PRODUCTION_SIGNALS, extra: 1 })
54
+ .success,
55
+ ).toBe(false);
56
+ expect(
57
+ ScheduleDefinitionSchema.safeParse({
58
+ ...PRODUCTION_SIGNALS,
59
+ cron: "0 0 31 2 *",
60
+ }).success,
61
+ ).toBe(false);
62
+ expect(
63
+ ScheduleDefinitionSchema.safeParse({
64
+ ...PRODUCTION_SIGNALS,
65
+ timezone: "America/New_York",
66
+ }).success,
67
+ ).toBe(false);
68
+ });
69
+
70
+ it("serializes the registry canonically regardless of input order", () => {
71
+ const other: ScheduleDefinition = {
72
+ ...PRODUCTION_SIGNALS,
73
+ id: "audit-rollup",
74
+ cron: "0 6 * * *",
75
+ };
76
+ const reordered = Object.fromEntries(
77
+ Object.entries(PRODUCTION_SIGNALS).reverse(),
78
+ ) as ScheduleDefinition;
79
+ expect(canonicalScheduleRegistryJson([PRODUCTION_SIGNALS, other])).toBe(
80
+ canonicalScheduleRegistryJson([other, reordered]),
81
+ );
82
+ expect(
83
+ canonicalScheduleRegistryJson([other]).startsWith(
84
+ '[{"id":"audit-rollup","repo"',
85
+ ),
86
+ ).toBe(true);
87
+ });
88
+
89
+ it("rejects a snapshot with duplicate ids", () => {
90
+ const result = ScheduleRegistrySnapshotSchema.safeParse({
91
+ schemaVersion: 1,
92
+ contentHash: "a".repeat(64),
93
+ schedules: [PRODUCTION_SIGNALS, PRODUCTION_SIGNALS],
94
+ });
95
+ expect(result.success).toBe(false);
96
+ });
97
+ });
@@ -0,0 +1,75 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ SCHEDULE_EXECUTION_STATES,
4
+ SCHEDULE_EXECUTION_TRANSITIONS,
5
+ canTransitionScheduleExecution,
6
+ isScheduleExecutionSettled,
7
+ mayReexecuteAutomatically,
8
+ scheduleIncidentKey,
9
+ } from "../execution";
10
+
11
+ describe("schedules execution lifecycle", () => {
12
+ it("allows no transition out of a terminal state", () => {
13
+ for (const terminal of [
14
+ "output_verified",
15
+ "superseded",
16
+ "missed",
17
+ "acknowledged_loss",
18
+ ] as const) {
19
+ expect(SCHEDULE_EXECUTION_TRANSITIONS[terminal]).toEqual([]);
20
+ }
21
+ for (const state of SCHEDULE_EXECUTION_STATES) {
22
+ for (const target of SCHEDULE_EXECUTION_TRANSITIONS[state]) {
23
+ expect(SCHEDULE_EXECUTION_STATES).toContain(target);
24
+ }
25
+ }
26
+ expect(
27
+ canTransitionScheduleExecution(
28
+ "dispatch_requested",
29
+ "dispatch_requested",
30
+ ),
31
+ ).toBe(true);
32
+ expect(canTransitionScheduleExecution("started", "superseded")).toBe(false);
33
+ });
34
+
35
+ it("settles a successful completion only without a verifier", () => {
36
+ expect(
37
+ isScheduleExecutionSettled({
38
+ state: "completed",
39
+ conclusion: "success",
40
+ requiresVerification: false,
41
+ }),
42
+ ).toBe(true);
43
+ expect(
44
+ isScheduleExecutionSettled({
45
+ state: "completed",
46
+ conclusion: "success",
47
+ requiresVerification: true,
48
+ }),
49
+ ).toBe(false);
50
+ expect(
51
+ isScheduleExecutionSettled({
52
+ state: "completed",
53
+ conclusion: "failure",
54
+ requiresVerification: false,
55
+ }),
56
+ ).toBe(false);
57
+ expect(
58
+ isScheduleExecutionSettled({
59
+ state: "timed_out",
60
+ conclusion: null,
61
+ requiresVerification: false,
62
+ }),
63
+ ).toBe(false);
64
+ });
65
+
66
+ it("never re-executes unsafe effects automatically", () => {
67
+ expect(mayReexecuteAutomatically("unsafe")).toBe(false);
68
+ for (const effects of ["none", "idempotent", "fenced"] as const) {
69
+ expect(mayReexecuteAutomatically(effects)).toBe(true);
70
+ }
71
+ expect(scheduleIncidentKey("delivery", "pin-audit")).toBe(
72
+ "schedule:delivery:pin-audit",
73
+ );
74
+ });
75
+ });
@@ -0,0 +1,51 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ displayTitleFor,
4
+ executionIdFor,
5
+ parseDisplayTitle,
6
+ parseExecutionId,
7
+ slotIso,
8
+ } from "../identity";
9
+
10
+ describe("schedules identity", () => {
11
+ it("round-trips an execution id and a display title", () => {
12
+ const slot = new Date("2026-10-10T10:00:00Z");
13
+ const executionId = executionIdFor("production-signals-signals", slot);
14
+ expect(executionId).toBe("production-signals-signals/2026-10-10T10:00:00Z");
15
+ expect(parseExecutionId(executionId)).toEqual({
16
+ scheduleId: "production-signals-signals",
17
+ slot,
18
+ });
19
+ expect(displayTitleFor(executionId, 2)).toBe(
20
+ "production-signals-signals/2026-10-10T10:00:00Z#2",
21
+ );
22
+ expect(parseDisplayTitle(displayTitleFor(executionId, 2))).toEqual({
23
+ scheduleId: "production-signals-signals",
24
+ slot,
25
+ executionId,
26
+ attempt: 2,
27
+ });
28
+ });
29
+
30
+ it("refuses a slot that is not a whole UTC minute", () => {
31
+ expect(() => slotIso(new Date("2026-10-10T10:00:30Z"))).toThrow(RangeError);
32
+ expect(parseExecutionId("x/2026-10-10T10:00:30Z")).toBeNull();
33
+ expect(() =>
34
+ executionIdFor("Bad/Id", new Date("2026-10-10T10:00:00Z")),
35
+ ).toThrow(RangeError);
36
+ expect(() => displayTitleFor("x/2026-10-10T10:00:00Z", 0)).toThrow(
37
+ RangeError,
38
+ );
39
+ });
40
+
41
+ it("does not parse a manual run title", () => {
42
+ for (const title of [
43
+ "Production signals",
44
+ "x/2026-10-10T10:00:00Z",
45
+ "x/2026-10-10T10:00:00Z#0",
46
+ "#1",
47
+ ]) {
48
+ expect(parseDisplayTitle(title)).toBeNull();
49
+ }
50
+ });
51
+ });
@@ -0,0 +1,26 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import * as root from "../../index";
3
+
4
+ describe("schedules exports", () => {
5
+ it("exports the scheduled-execution vocabulary from the package root", () => {
6
+ for (const name of [
7
+ "parseCron",
8
+ "previousSlot",
9
+ "slotsBetween",
10
+ "executionIdFor",
11
+ "parseDisplayTitle",
12
+ "SCHEDULE_RUN_NAME",
13
+ "SCHEDULE_DISPATCH_INPUTS",
14
+ "SCHEDULE_EXECUTION_TRANSITIONS",
15
+ "SCHEDULE_EFFECTS",
16
+ "ScheduleDefinitionSchema",
17
+ "ScheduleRegistrySnapshotSchema",
18
+ "canonicalScheduleRegistryJson",
19
+ "ScheduleClaimResponseSchema",
20
+ "SCHEDULE_TICK_PATH",
21
+ "SCHEDULE_METRICS_NAMESPACE",
22
+ ]) {
23
+ expect(root).toHaveProperty(name);
24
+ }
25
+ });
26
+ });