@popoverai/dotrequirements 0.39.2 → 0.39.4
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/dist/commands/report.d.ts +6 -0
- package/dist/commands/report.js +20 -12
- package/dist/rehearsal/derive.js +5 -0
- package/dist/rehearsal/types.d.ts +2 -0
- package/dist/schema/browser.d.ts +1 -0
- package/dist/schema/browser.js +3 -0
- package/dist/schema/time.d.ts +21 -0
- package/dist/schema/time.js +71 -0
- package/package.json +2 -2
|
@@ -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 };
|
package/dist/commands/report.js
CHANGED
|
@@ -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 =
|
|
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} — ${
|
|
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}] — ${
|
|
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 =
|
|
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=${
|
|
250
|
+
parts.push(`since=${formatInstant(filters.since, timeZone)}`);
|
|
243
251
|
}
|
|
244
252
|
return parts.length > 0 ? `Filters: ${parts.join(", ")}` : "";
|
|
245
253
|
}
|
package/dist/rehearsal/derive.js
CHANGED
|
@@ -121,6 +121,11 @@ export function planForOutcomes(plan, names) {
|
|
|
121
121
|
if (surface.mergeCloses !== undefined && !kept.has(surface.mergeCloses)) {
|
|
122
122
|
delete own.mergeCloses;
|
|
123
123
|
}
|
|
124
|
+
// PULL-20.6: nor does approving a checkpoint left out merge it.
|
|
125
|
+
if (surface.mergeOn !== undefined &&
|
|
126
|
+
!checkpoints.some((checkpoint) => checkpoint.name === surface.mergeOn)) {
|
|
127
|
+
delete own.mergeOn;
|
|
128
|
+
}
|
|
124
129
|
return own;
|
|
125
130
|
}),
|
|
126
131
|
phases,
|
|
@@ -105,6 +105,8 @@ export type Plan = {
|
|
|
105
105
|
watchedBy?: string;
|
|
106
106
|
/** The outcome a merge of its pull request closes (PULL-12). */
|
|
107
107
|
mergeCloses?: string;
|
|
108
|
+
/** The checkpoint whose approval merges its pull request (PULL-20). */
|
|
109
|
+
mergeOn?: string;
|
|
108
110
|
}>;
|
|
109
111
|
phases: Array<{
|
|
110
112
|
name: string;
|
package/dist/schema/browser.d.ts
CHANGED
|
@@ -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
|
package/dist/schema/browser.js
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "0.39.4",
|
|
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": "^
|
|
79
|
+
"vitest": "^5.0.5"
|
|
80
80
|
},
|
|
81
81
|
"files": [
|
|
82
82
|
"dist",
|