@popoverai/dotrequirements 0.39.2 → 0.39.3

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.
@@ -5,6 +5,12 @@ interface ReportOptions {
5
5
  requirement?: string;
6
6
  branch?: string;
7
7
  since?: number;
8
+ /**
9
+ * The zone the console and Markdown reports write times in; the machine's
10
+ * when left out, as the command always leaves it (REPORT-CLOUD-2). Given
11
+ * by tests, so they don't depend on the zone they run in.
12
+ */
13
+ timeZone?: string;
8
14
  }
9
15
  export declare function reportCommand(options: ReportOptions): Promise<void>;
10
16
  export type { CoverageEntry };
@@ -3,8 +3,13 @@ import { findRequirementsDir, readCoverageCache } from "../harness/cache.js";
3
3
  import { formatUnrecordedNotice } from "../harness/reportingStatus.js";
4
4
  import { getProjectCoverage, getRequirementCoverage, } from "../requirements/cloud-coverage.js";
5
5
  import { buildLocalReport, } from "../requirements/coverage.js";
6
+ import { formatInstant } from "../schema/time.js";
6
7
  import { findProjectRoot, getProjectCredentials, } from "../utils/project-settings.js";
7
8
  import { markVersionMessagesShown, versionMessages, } from "../version-messages.js";
9
+ /** The zone this machine is set to, as "America/Los_Angeles". */
10
+ function machineZone() {
11
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || undefined;
12
+ }
8
13
  export async function reportCommand(options) {
9
14
  const source = options.source ?? "local";
10
15
  const format = options.format ?? "console";
@@ -39,6 +44,9 @@ export async function reportCommand(options) {
39
44
  return;
40
45
  }
41
46
  // source === "cloud"
47
+ // REPORT-CLOUD-2: what Casey reads is in their machine's zone; the JSON
48
+ // keeps the timestamps as recorded (2.4).
49
+ const timeZone = options.timeZone ?? machineZone();
42
50
  let projectId;
43
51
  let projectSecret;
44
52
  try {
@@ -60,7 +68,7 @@ export async function reportCommand(options) {
60
68
  console.log(printCloudRequirement(record, format, {
61
69
  branch: options.branch,
62
70
  since: options.since,
63
- }, format === "json" ? await versionMessages() : []));
71
+ }, timeZone, format === "json" ? await versionMessages() : []));
64
72
  if (format === "json")
65
73
  markVersionMessagesShown();
66
74
  return;
@@ -72,7 +80,7 @@ export async function reportCommand(options) {
72
80
  console.log(printCloudProject(record, format, {
73
81
  branch: options.branch,
74
82
  since: options.since,
75
- }, format === "json" ? await versionMessages() : []));
83
+ }, timeZone, format === "json" ? await versionMessages() : []));
76
84
  if (format === "json")
77
85
  markVersionMessagesShown();
78
86
  }
@@ -127,7 +135,7 @@ function formatLocalMarkdown(report) {
127
135
  }
128
136
  return out;
129
137
  }
130
- function printCloudRequirement(record, format, filters, messages = []) {
138
+ function printCloudRequirement(record, format, filters, timeZone, messages = []) {
131
139
  if (format === "json") {
132
140
  // CLI-MSG-1.1: the cloud's messages for this version travel in the JSON.
133
141
  return JSON.stringify({ source: "cloud", requirement: record, messages }, null, 2);
@@ -136,13 +144,13 @@ function printCloudRequirement(record, format, filters, messages = []) {
136
144
  // REPORT-CLOUD-1.7: with filters in play, an empty record means nothing
137
145
  // matched them — say so rather than implying the requirement was never
138
146
  // tested at all
139
- const filterNote = formatCloudFilters(filters);
147
+ const filterNote = formatCloudFilters(filters, timeZone);
140
148
  if (filterNote) {
141
149
  return `\n${record.requirementKey}: no coverage records match the requested filters (${filterNote.replace(/^Filters: /, "")}).\n`;
142
150
  }
143
151
  return `\n${record.requirementKey}: never tested on the cloud-persisted record.\n`;
144
152
  }
145
- const lastTested = new Date(record.lastTestedAt).toISOString();
153
+ const lastTested = formatInstant(record.lastTestedAt, timeZone);
146
154
  const lines = [];
147
155
  lines.push(`\n=== Coverage for ${record.requirementKey} (cloud) ===\n`);
148
156
  lines.push(`Last tested: ${lastTested}`);
@@ -156,7 +164,7 @@ function printCloudRequirement(record, format, filters, messages = []) {
156
164
  for (const b of record.allBranches) {
157
165
  if (b.branch === record.branch)
158
166
  continue;
159
- lines.push(` ${b.branch} — ${new Date(b.lastTestedAt).toISOString()}${b.testFile ? ` (${b.testFile}${b.testLine ? `:${b.testLine}` : ""})` : ""}`);
167
+ lines.push(` ${b.branch} — ${formatInstant(b.lastTestedAt, timeZone)}${b.testFile ? ` (${b.testFile}${b.testLine ? `:${b.testLine}` : ""})` : ""}`);
160
168
  }
161
169
  }
162
170
  lines.push("");
@@ -174,7 +182,7 @@ function printCloudRequirement(record, format, filters, messages = []) {
174
182
  }
175
183
  return lines.join("\n");
176
184
  }
177
- function printCloudProject(record, format, filters, messages = []) {
185
+ function printCloudProject(record, format, filters, timeZone, messages = []) {
178
186
  if (format === "json") {
179
187
  // CLI-MSG-1.1: the cloud's messages for this version travel in the JSON.
180
188
  return JSON.stringify({
@@ -186,7 +194,7 @@ function printCloudProject(record, format, filters, messages = []) {
186
194
  }, null, 2);
187
195
  }
188
196
  const summary = cloudSummary(record);
189
- const filterNote = formatCloudFilters(filters);
197
+ const filterNote = formatCloudFilters(filters, timeZone);
190
198
  if (format === "markdown") {
191
199
  let out = `# Requirements Coverage Report (cloud)\n\n`;
192
200
  if (filterNote)
@@ -195,7 +203,7 @@ function printCloudProject(record, format, filters, messages = []) {
195
203
  if (record.tested.length > 0) {
196
204
  out += `## Tested\n\n`;
197
205
  for (const t of record.tested) {
198
- out += `- **${t.requirementKey}** [${t.branch}] — ${new Date(t.lastTestedAt).toISOString()}\n`;
206
+ out += `- **${t.requirementKey}** [${t.branch}] — ${formatInstant(t.lastTestedAt, timeZone)}\n`;
199
207
  }
200
208
  out += "\n";
201
209
  }
@@ -214,7 +222,7 @@ function printCloudProject(record, format, filters, messages = []) {
214
222
  if (record.tested.length > 0) {
215
223
  out += "\n✓ Tested:\n";
216
224
  for (const t of record.tested) {
217
- const date = new Date(t.lastTestedAt).toISOString();
225
+ const date = formatInstant(t.lastTestedAt, timeZone);
218
226
  out += ` ${t.requirementKey} [${t.branch}] — ${date}\n`;
219
227
  }
220
228
  }
@@ -234,12 +242,12 @@ function cloudSummary(record) {
234
242
  const coveragePercent = total > 0 ? parseFloat(((tested / total) * 100).toFixed(1)) : 0;
235
243
  return { total, tested, untested, coveragePercent };
236
244
  }
237
- function formatCloudFilters(filters) {
245
+ function formatCloudFilters(filters, timeZone) {
238
246
  const parts = [];
239
247
  if (filters.branch)
240
248
  parts.push(`branch=${filters.branch}`);
241
249
  if (filters.since !== undefined) {
242
- parts.push(`since=${new Date(filters.since).toISOString()}`);
250
+ parts.push(`since=${formatInstant(filters.since, timeZone)}`);
243
251
  }
244
252
  return parts.length > 0 ? `Filters: ${parts.join(", ")}` : "";
245
253
  }
@@ -14,4 +14,5 @@ export { composeMarkdownWithTitle, effectiveTitle, type SplitMarkdown, splitLead
14
14
  export { attachmentListsEqual } from "./attachment-lists.js";
15
15
  export type { BuildScenarioOptions, Scenario, ScenarioAssertionSource, ScenarioStep, } from "./scenario.js";
16
16
  export { buildScenarioFromRequirements, requirementTreeToScenario, } from "./scenario.js";
17
+ export { formatDay, formatInstant, isTimeZone } from "./time.js";
17
18
  //# sourceMappingURL=browser.d.ts.map
@@ -39,4 +39,7 @@ export { composeMarkdownWithTitle, effectiveTitle, splitLeadingH1, } from "./tit
39
39
  export { attachmentListsEqual } from "./attachment-lists.js";
40
40
  // Scenario building (pure TypeScript - browser-safe, used by Convex Node actions)
41
41
  export { buildScenarioFromRequirements, requirementTreeToScenario, } from "./scenario.js";
42
+ // How a time is written for a person to read (LOCALTIME-2), shared with the
43
+ // backend so the web's tools and Convex write the same string
44
+ export { formatDay, formatInstant, isTimeZone } from "./time.js";
42
45
  //# sourceMappingURL=browser.js.map
@@ -0,0 +1,21 @@
1
+ /**
2
+ * How the product writes a time for a person to read (LOCALTIME-2): the
3
+ * date, the time to the minute, and the zone by name, in the reader's zone,
4
+ * or in UTC, named as UTC, when the product hasn't seen it (2.3).
5
+ *
6
+ * The one formatter: the backend imports it by path for the text it writes
7
+ * for agents, the web for what its tools hand chat agents, and the CLI for
8
+ * its report (REPORT-CLOUD-2), so all three write the same string for the
9
+ * same moment. Dependency-free and browser-safe.
10
+ */
11
+ /**
12
+ * An instant in a zone, as "Oct 1, 2026, 11:43 PM PDT": the zone as it
13
+ * stood at that moment, its US English abbreviation, or its offset where it
14
+ * has none, as "GMT+5:30" (LOCALTIME-2.1, 2.2). With no zone, UTC.
15
+ */
16
+ export declare function formatInstant(at: number, timeZone: string | undefined): string;
17
+ /** The day an instant falls on in a zone, as "Oct 1, 2026" (LOCALTIME-2.4). With no zone, UTC's. */
18
+ export declare function formatDay(at: number, timeZone: string | undefined): string;
19
+ /** Whether a string names a zone `Intl` accepts, as "America/Los_Angeles". */
20
+ export declare function isTimeZone(name: string): boolean;
21
+ //# sourceMappingURL=time.d.ts.map
@@ -0,0 +1,71 @@
1
+ /**
2
+ * How the product writes a time for a person to read (LOCALTIME-2): the
3
+ * date, the time to the minute, and the zone by name, in the reader's zone,
4
+ * or in UTC, named as UTC, when the product hasn't seen it (2.3).
5
+ *
6
+ * The one formatter: the backend imports it by path for the text it writes
7
+ * for agents, the web for what its tools hand chat agents, and the CLI for
8
+ * its report (REPORT-CLOUD-2), so all three write the same string for the
9
+ * same moment. Dependency-free and browser-safe.
10
+ */
11
+ const INSTANT = {
12
+ month: "short",
13
+ day: "numeric",
14
+ year: "numeric",
15
+ hour: "numeric",
16
+ minute: "2-digit",
17
+ timeZoneName: "short",
18
+ };
19
+ const DAY = {
20
+ month: "short",
21
+ day: "numeric",
22
+ year: "numeric",
23
+ };
24
+ /**
25
+ * Some ICU versions put a narrow no-break space before AM/PM. Written as a
26
+ * plain space, so every runtime gives the same string.
27
+ */
28
+ function plain(text) {
29
+ return text.replace(/[\u202f\u00a0]/g, " ");
30
+ }
31
+ /**
32
+ * A formatter in the zone, or in UTC when this runtime doesn't know it: a
33
+ * zone is checked before it is stored, but a runtime with other zone data
34
+ * than the one that checked it would otherwise fail a whole tool call.
35
+ */
36
+ function formatterIn(options, timeZone) {
37
+ try {
38
+ return new Intl.DateTimeFormat("en-US", {
39
+ ...options,
40
+ timeZone: timeZone ?? "UTC",
41
+ });
42
+ }
43
+ catch {
44
+ return new Intl.DateTimeFormat("en-US", { ...options, timeZone: "UTC" });
45
+ }
46
+ }
47
+ /**
48
+ * An instant in a zone, as "Oct 1, 2026, 11:43 PM PDT": the zone as it
49
+ * stood at that moment, its US English abbreviation, or its offset where it
50
+ * has none, as "GMT+5:30" (LOCALTIME-2.1, 2.2). With no zone, UTC.
51
+ */
52
+ export function formatInstant(at, timeZone) {
53
+ return plain(formatterIn(INSTANT, timeZone).format(at));
54
+ }
55
+ /** The day an instant falls on in a zone, as "Oct 1, 2026" (LOCALTIME-2.4). With no zone, UTC's. */
56
+ export function formatDay(at, timeZone) {
57
+ return plain(formatterIn(DAY, timeZone).format(at));
58
+ }
59
+ /** Whether a string names a zone `Intl` accepts, as "America/Los_Angeles". */
60
+ export function isTimeZone(name) {
61
+ if (!name)
62
+ return false;
63
+ try {
64
+ new Intl.DateTimeFormat("en-US", { timeZone: name });
65
+ return true;
66
+ }
67
+ catch {
68
+ return false;
69
+ }
70
+ }
71
+ //# sourceMappingURL=time.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@popoverai/dotrequirements",
3
- "version": "0.39.2",
3
+ "version": "0.39.3",
4
4
  "description": "Requirements as testable, human-readable data — CLI and test harness for spec-driven development with AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -76,7 +76,7 @@
76
76
  "@types/node": "^22",
77
77
  "@types/uuid": "^11.0.0",
78
78
  "typescript": "^5",
79
- "vitest": "^4.1.10"
79
+ "vitest": "^5.0.5"
80
80
  },
81
81
  "files": [
82
82
  "dist",