@popoverai/dotrequirements 0.32.1 → 0.34.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/dist/cli.js +32 -7
- package/dist/cloud.d.ts +18 -0
- package/dist/cloud.js +38 -0
- package/dist/commands/board/index.d.ts +13 -0
- package/dist/commands/board/index.js +96 -0
- package/dist/commands/branch-merge.d.ts +78 -0
- package/dist/commands/branch-merge.js +129 -0
- package/dist/commands/create-requirement-document.js +2 -2
- package/dist/commands/diff.js +15 -5
- package/dist/commands/init.js +7 -6
- package/dist/commands/link.js +27 -21
- package/dist/commands/mockup/index.d.ts +11 -0
- package/dist/commands/mockup/index.js +82 -0
- package/dist/commands/report.js +22 -9
- package/dist/commands/review-test.js +3 -2
- package/dist/commands/scores/index.d.ts +50 -0
- package/dist/commands/scores/index.js +190 -0
- package/dist/commands/solution/ask.d.ts +9 -0
- package/dist/commands/solution/ask.js +27 -0
- package/dist/commands/solution/author.d.ts +19 -0
- package/dist/commands/solution/author.js +202 -0
- package/dist/commands/solution/branch.d.ts +103 -0
- package/dist/commands/solution/branch.js +341 -0
- package/dist/commands/solution/brief.d.ts +81 -0
- package/dist/commands/solution/brief.js +47 -0
- package/dist/commands/solution/checkout.d.ts +27 -0
- package/dist/commands/solution/checkout.js +66 -0
- package/dist/commands/solution/client.d.ts +16 -0
- package/dist/commands/solution/client.js +46 -0
- package/dist/commands/solution/contract.d.ts +9 -0
- package/dist/commands/solution/contract.js +23 -0
- package/dist/commands/solution/create.d.ts +14 -0
- package/dist/commands/solution/create.js +52 -0
- package/dist/commands/solution/demo-script.d.ts +28 -0
- package/dist/commands/solution/demo-script.js +43 -0
- package/dist/commands/solution/image.d.ts +60 -0
- package/dist/commands/solution/image.js +148 -0
- package/dist/commands/solution/index.d.ts +12 -0
- package/dist/commands/solution/index.js +281 -0
- package/dist/commands/solution/look-up.d.ts +9 -0
- package/dist/commands/solution/look-up.js +69 -0
- package/dist/commands/solution/next.d.ts +12 -0
- package/dist/commands/solution/next.js +48 -0
- package/dist/commands/solution/plan-file.d.ts +22 -0
- package/dist/commands/solution/plan-file.js +33 -0
- package/dist/commands/solution/preview-screenshot.d.ts +21 -0
- package/dist/commands/solution/preview-screenshot.js +56 -0
- package/dist/commands/solution/pull-request.d.ts +25 -0
- package/dist/commands/solution/pull-request.js +117 -0
- package/dist/commands/solution/pushing.d.ts +66 -0
- package/dist/commands/solution/pushing.js +185 -0
- package/dist/commands/solution/rehearse.d.ts +10 -0
- package/dist/commands/solution/rehearse.js +24 -0
- package/dist/commands/solution/request.d.ts +3 -0
- package/dist/commands/solution/request.js +13 -0
- package/dist/commands/solution/review.d.ts +27 -0
- package/dist/commands/solution/review.js +104 -0
- package/dist/commands/solution/run-file.d.ts +42 -0
- package/dist/commands/solution/run-file.js +63 -0
- package/dist/commands/solution/share.d.ts +62 -0
- package/dist/commands/solution/share.js +346 -0
- package/dist/commands/solution/stall.d.ts +14 -0
- package/dist/commands/solution/stall.js +41 -0
- package/dist/commands/solution/start.d.ts +11 -0
- package/dist/commands/solution/start.js +20 -0
- package/dist/commands/solution/status.d.ts +39 -0
- package/dist/commands/solution/status.js +145 -0
- package/dist/commands/solution/summary.d.ts +13 -0
- package/dist/commands/solution/summary.js +5 -0
- package/dist/commands/solution/things.d.ts +118 -0
- package/dist/commands/solution/things.js +78 -0
- package/dist/commands/solution/thread.d.ts +13 -0
- package/dist/commands/solution/thread.js +53 -0
- package/dist/commands/solution/upload.d.ts +24 -0
- package/dist/commands/solution/upload.js +27 -0
- package/dist/commands/solution/video.d.ts +88 -0
- package/dist/commands/solution/video.js +364 -0
- package/dist/commands/solution/working-folder.d.ts +44 -0
- package/dist/commands/solution/working-folder.js +64 -0
- package/dist/commands/style-check.js +4 -4
- package/dist/commands/sync.d.ts +2 -1
- package/dist/commands/sync.js +21 -5
- package/dist/convex.d.ts +64 -10
- package/dist/convex.js +75 -10
- package/dist/harness/finalize.js +16 -8
- package/dist/harness/reportingStatus.js +2 -11
- package/dist/push/core.js +7 -2
- package/dist/rehearsal/derive.d.ts +23 -0
- package/dist/rehearsal/derive.js +112 -0
- package/dist/rehearsal/driver.d.ts +44 -0
- package/dist/rehearsal/driver.js +337 -0
- package/dist/rehearsal/earlier.d.ts +25 -0
- package/dist/rehearsal/earlier.js +138 -0
- package/dist/rehearsal/index.d.ts +5 -0
- package/dist/rehearsal/index.js +4 -0
- package/dist/rehearsal/live.d.ts +72 -0
- package/dist/rehearsal/live.js +301 -0
- package/dist/rehearsal/report.d.ts +4 -0
- package/dist/rehearsal/report.js +69 -0
- package/dist/rehearsal/types.d.ts +377 -0
- package/dist/rehearsal/types.js +8 -0
- package/dist/requirements/cloud-ai.d.ts +0 -6
- package/dist/requirements/cloud-ai.js +2 -11
- package/dist/requirements/cloud-coverage.js +4 -4
- package/dist/requirements/index.d.ts +6 -0
- package/dist/requirements/index.js +24 -0
- package/dist/schema/browser.d.ts +1 -1
- package/dist/schema/browser.js +3 -1
- package/dist/schema/file-writer.d.ts +5 -0
- package/dist/schema/file-writer.js +4 -0
- package/dist/schema/parser.d.ts +11 -2
- package/dist/schema/parser.js +11 -7
- package/dist/schema/schemas.d.ts +40 -0
- package/dist/schema/schemas.js +4 -0
- package/dist/sync/execute.d.ts +10 -1
- package/dist/sync/execute.js +76 -24
- package/dist/sync/index.d.ts +1 -1
- package/dist/sync/index.js +1 -1
- package/dist/sync/publish.d.ts +7 -0
- package/dist/sync/publish.js +2 -2
- package/dist/sync/segment.js +4 -1
- package/dist/sync/snapshot.d.ts +27 -5
- package/dist/sync/snapshot.js +92 -15
- package/dist/sync/solution-documents.d.ts +64 -0
- package/dist/sync/solution-documents.js +97 -0
- package/dist/sync/types.d.ts +19 -1
- package/dist/utils/command-error.d.ts +19 -0
- package/dist/utils/command-error.js +44 -0
- package/dist/utils/oauth-flow.js +2 -2
- package/dist/utils/project-selector.js +4 -4
- package/dist/version-messages.d.ts +28 -0
- package/dist/version-messages.js +87 -0
- package/package.json +3 -2
- package/dist/config.d.ts +0 -5
- package/dist/config.js +0 -12
package/dist/sync/snapshot.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Snapshot acquisition for the comparator — kept separate from comparison so
|
|
3
3
|
* the comparator stays a pure function (compare.ts). The local half reads the
|
|
4
|
-
* workspace; the cloud half is one
|
|
4
|
+
* workspace; the cloud half is one export query.
|
|
5
5
|
*/
|
|
6
6
|
import * as fs from "node:fs";
|
|
7
7
|
import { ConvexHttpClient } from "convex/browser";
|
|
8
8
|
import YAML from "yaml";
|
|
9
|
-
import {
|
|
9
|
+
import { convexUrl } from "../cloud.js";
|
|
10
10
|
import { api } from "../convex.js";
|
|
11
|
-
import { findRequirementsFiles } from "../requirements/index.js";
|
|
11
|
+
import { findRequirementsFiles, findSyncedFiles, } from "../requirements/index.js";
|
|
12
12
|
import { splitLeadingH1, validateMetadata } from "../schema/index.js";
|
|
13
13
|
import { parseRequirementBlocksFromMarkdown, splitFrontmatter, stripFrontmatterBlock, } from "../schema/parser-core.js";
|
|
14
14
|
/**
|
|
@@ -33,6 +33,8 @@ export function readLocalDocument(filePath) {
|
|
|
33
33
|
let title;
|
|
34
34
|
let defaultPrefix;
|
|
35
35
|
let attachments;
|
|
36
|
+
let solution;
|
|
37
|
+
let outcome;
|
|
36
38
|
let frontmatterError;
|
|
37
39
|
const split = splitFrontmatter(raw);
|
|
38
40
|
if (split) {
|
|
@@ -57,6 +59,11 @@ export function readLocalDocument(filePath) {
|
|
|
57
59
|
title = d.title;
|
|
58
60
|
if (typeof d.defaultPrefix === "string")
|
|
59
61
|
defaultPrefix = d.defaultPrefix;
|
|
62
|
+
// SOLDOC-5: the belonging, read like the id — before validation.
|
|
63
|
+
if (typeof d.solution === "string")
|
|
64
|
+
solution = d.solution;
|
|
65
|
+
if (typeof d.outcome === "string")
|
|
66
|
+
outcome = d.outcome;
|
|
60
67
|
// ATTACH-10.1: attachments from frontmatter. Shape-filtered here for
|
|
61
68
|
// the same reason as the fields above; a malformed entry is caught by
|
|
62
69
|
// validateMetadata below and flags the file rather than half-syncing.
|
|
@@ -108,6 +115,8 @@ export function readLocalDocument(filePath) {
|
|
|
108
115
|
title: derivedTitle,
|
|
109
116
|
defaultPrefix,
|
|
110
117
|
attachments,
|
|
118
|
+
solution,
|
|
119
|
+
outcome,
|
|
111
120
|
body,
|
|
112
121
|
};
|
|
113
122
|
}
|
|
@@ -115,26 +124,66 @@ export function readLocalDocument(filePath) {
|
|
|
115
124
|
* Build the local snapshot from the workspace, or from an explicit set of file
|
|
116
125
|
* paths (scope). Files are always read fresh from disk.
|
|
117
126
|
*/
|
|
118
|
-
export async function acquireLocalSnapshot(workspaceRoot, filePaths) {
|
|
119
|
-
|
|
127
|
+
export async function acquireLocalSnapshot(workspaceRoot, filePaths, options = {}) {
|
|
128
|
+
// SOLACCESS-2.2: a project whose team doesn't have solutions syncs the
|
|
129
|
+
// files it always did — solution directories' Markdown is left alone.
|
|
130
|
+
const paths = filePaths ??
|
|
131
|
+
(options.solutions === false
|
|
132
|
+
? await findRequirementsFiles(workspaceRoot)
|
|
133
|
+
: await findSyncedFiles(workspaceRoot));
|
|
120
134
|
return { documents: paths.map(readLocalDocument) };
|
|
121
135
|
}
|
|
122
136
|
/**
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
137
|
+
* The project's solutions as the sync places files by them (SOLDOC-4), or
|
|
138
|
+
* null when the project's team doesn't have solutions, so the sync behaves
|
|
139
|
+
* as it did before solutions (SOLACCESS-2.2). Whether it has them is the
|
|
140
|
+
* export's answer — the one the cloud side was read with — so the local
|
|
141
|
+
* side can't follow a different one. A share run places nothing, so it
|
|
142
|
+
* needs no list. A failure to fetch the list fails the sync, as does a
|
|
143
|
+
* switch flipped between the two reads: guessing would leave solution
|
|
144
|
+
* documents out of the repo's side, and a repo-wins sync would delete them
|
|
145
|
+
* from the cloud.
|
|
146
|
+
*/
|
|
147
|
+
export async function projectSolutions(auth, cloud) {
|
|
148
|
+
if (cloud.solutions !== true)
|
|
149
|
+
return null;
|
|
150
|
+
if (!auth.projectSlug)
|
|
151
|
+
return [];
|
|
152
|
+
const client = new ConvexHttpClient(convexUrl());
|
|
153
|
+
const answer = await client.query(api.solutions.queries.forSync, {
|
|
154
|
+
projectAuth: {
|
|
155
|
+
projectSlug: auth.projectSlug,
|
|
156
|
+
projectSecret: auth.projectSecret,
|
|
157
|
+
},
|
|
158
|
+
project: auth.projectSlug,
|
|
159
|
+
});
|
|
160
|
+
if (!Array.isArray(answer)) {
|
|
161
|
+
throw new Error("The project's solutions were switched off while syncing. Run the sync again.");
|
|
162
|
+
}
|
|
163
|
+
return answer;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Build the cloud snapshot with one export query: the project's own, with a
|
|
167
|
+
* project secret, or the share endpoint, with a bare share token (the token
|
|
168
|
+
* names the project) — the read-only path never writes back.
|
|
126
169
|
*/
|
|
127
170
|
export async function acquireCloudSnapshot(auth) {
|
|
128
|
-
const client = new ConvexHttpClient(
|
|
129
|
-
const
|
|
130
|
-
?
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
171
|
+
const client = new ConvexHttpClient(convexUrl());
|
|
172
|
+
const result = (await (auth.projectSlug
|
|
173
|
+
? client.query(api.documents.queries.exportProject, {
|
|
174
|
+
projectAuth: {
|
|
175
|
+
projectSlug: auth.projectSlug,
|
|
176
|
+
projectSecret: auth.projectSecret,
|
|
177
|
+
},
|
|
178
|
+
project: auth.projectSlug,
|
|
179
|
+
})
|
|
180
|
+
: client.query(api.documents.queries.exportProjectForShare, {
|
|
181
|
+
shareToken: auth.projectSecret,
|
|
182
|
+
})));
|
|
135
183
|
return {
|
|
136
184
|
projectName: result.projectName,
|
|
137
185
|
projectSlug: result.projectSlug,
|
|
186
|
+
solutions: result.solutions === true,
|
|
138
187
|
documents: result.documents.map((d) => ({
|
|
139
188
|
documentId: d.documentId,
|
|
140
189
|
title: d.title,
|
|
@@ -143,7 +192,35 @@ export async function acquireCloudSnapshot(auth) {
|
|
|
143
192
|
body: d.markdownContent,
|
|
144
193
|
version: d.version,
|
|
145
194
|
updatedAt: d.updatedAt,
|
|
195
|
+
solution: d.solution,
|
|
196
|
+
isRequirementsDoc: d.isRequirementsDoc,
|
|
146
197
|
})),
|
|
147
198
|
};
|
|
148
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* SYNC-FAIL-5: of the ids the workspace's files carry, the ones that name
|
|
202
|
+
* another project's document, each with the words a push of that file gets,
|
|
203
|
+
* so `dotreq diff` says what `dotreq sync` will. A backend that can't answer
|
|
204
|
+
* leaves the diff as it was rather than failing it.
|
|
205
|
+
*/
|
|
206
|
+
export async function foreignDocumentNotes(auth, documentIds) {
|
|
207
|
+
// A share token only syncs down, so it pushes nothing to warn about.
|
|
208
|
+
if (documentIds.length === 0 || !auth.projectSlug)
|
|
209
|
+
return new Map();
|
|
210
|
+
const client = new ConvexHttpClient(convexUrl());
|
|
211
|
+
try {
|
|
212
|
+
const foreign = (await client.query(api.documents.queries.foreignDocumentIds, {
|
|
213
|
+
projectAuth: {
|
|
214
|
+
projectSlug: auth.projectSlug,
|
|
215
|
+
projectSecret: auth.projectSecret,
|
|
216
|
+
},
|
|
217
|
+
project: auth.projectSlug,
|
|
218
|
+
documentIds,
|
|
219
|
+
}));
|
|
220
|
+
return new Map(foreign.map((f) => [f.documentId, f.message]));
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return new Map();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
149
226
|
//# sourceMappingURL=snapshot.js.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a solution's documents live, the sync's half (SOLDOC; the decision
|
|
3
|
+
* is in docs/working/four-areas-prototype.md, "Where a solution's documents
|
|
4
|
+
* live"). A document that belongs to a solution and is not a requirements
|
|
5
|
+
* document lives in a solution directory, `.requirements/solutions/<solution>/`,
|
|
6
|
+
* named for its outcome; the `.requirements.md` name is what makes a file a
|
|
7
|
+
* requirements document, wherever it sits. A new file declares its belonging
|
|
8
|
+
* by its path; once the frontmatter carries it, the frontmatter is the truth.
|
|
9
|
+
*/
|
|
10
|
+
export declare const SOLUTIONS_DIR = "solutions";
|
|
11
|
+
/** What a file's place in the workspace says about it, before any frontmatter. */
|
|
12
|
+
export interface PathClaim {
|
|
13
|
+
/** The solution directory's name and the file's stem, as slugs. */
|
|
14
|
+
solutionSlug: string;
|
|
15
|
+
outcomeSlug: string;
|
|
16
|
+
}
|
|
17
|
+
/** The solution directory a file sits directly in, if any (SOLDOC-8.1). */
|
|
18
|
+
export declare function solutionDirOf(filePath: string): PathClaim | undefined;
|
|
19
|
+
/** `pitch` for `pitch.md` and for `pitch.requirements.md`. */
|
|
20
|
+
export declare function stemOf(fileName: string): string;
|
|
21
|
+
/** SOLDOC-3: the name, never the content. Undefined for a file the rule doesn't reach. */
|
|
22
|
+
export declare function requirementsDocByName(filePath: string): boolean | undefined;
|
|
23
|
+
export interface SolutionForSync {
|
|
24
|
+
solutionId: string;
|
|
25
|
+
title: string;
|
|
26
|
+
/**
|
|
27
|
+
* The solution's directory name, which no other solution in the project
|
|
28
|
+
* has (SOLDOC-9). A backend from before it was sent leaves it out, and the
|
|
29
|
+
* title's slug stands in.
|
|
30
|
+
*/
|
|
31
|
+
directory?: string;
|
|
32
|
+
outcomes: string[];
|
|
33
|
+
}
|
|
34
|
+
/** The directory a solution's documents live in, by name. */
|
|
35
|
+
export declare function directoryOf(solution: SolutionForSync): string;
|
|
36
|
+
export interface Belonging {
|
|
37
|
+
solutionId: string;
|
|
38
|
+
outcome: string;
|
|
39
|
+
}
|
|
40
|
+
export type Resolved = {
|
|
41
|
+
belonging: Belonging;
|
|
42
|
+
warning?: string;
|
|
43
|
+
} | {
|
|
44
|
+
belonging: undefined;
|
|
45
|
+
warning?: string;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* What a pushed file belongs to (SOLDOC-4, SOLDOC-5): the frontmatter's
|
|
49
|
+
* belonging when it has one, else what its path claims, resolved against
|
|
50
|
+
* the project's solutions. A frontmatter that disagrees with the path is
|
|
51
|
+
* kept and reported; a path that resolves to nothing refuses the push.
|
|
52
|
+
*/
|
|
53
|
+
export declare function resolveBelonging(filePath: string, frontmatter: {
|
|
54
|
+
solution?: string;
|
|
55
|
+
outcome?: string;
|
|
56
|
+
}, claim: PathClaim | undefined, solutions: SolutionForSync[] | undefined): Resolved;
|
|
57
|
+
/**
|
|
58
|
+
* SOLDOC-6, SOLDOC-9: where a pulled document that belongs to a solution
|
|
59
|
+
* lands when it has no file yet: in the solution's own directory, named for
|
|
60
|
+
* its outcome, with the `.requirements.md` name when it is a requirements
|
|
61
|
+
* document.
|
|
62
|
+
*/
|
|
63
|
+
export declare function solutionDocumentPath(requirementsDir: string, solutionDirectory: string, outcome: string, isRequirementsDoc: boolean): string;
|
|
64
|
+
//# sourceMappingURL=solution-documents.d.ts.map
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a solution's documents live, the sync's half (SOLDOC; the decision
|
|
3
|
+
* is in docs/working/four-areas-prototype.md, "Where a solution's documents
|
|
4
|
+
* live"). A document that belongs to a solution and is not a requirements
|
|
5
|
+
* document lives in a solution directory, `.requirements/solutions/<solution>/`,
|
|
6
|
+
* named for its outcome; the `.requirements.md` name is what makes a file a
|
|
7
|
+
* requirements document, wherever it sits. A new file declares its belonging
|
|
8
|
+
* by its path; once the frontmatter carries it, the frontmatter is the truth.
|
|
9
|
+
*/
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
import { sanitizeFileName } from "./local-files.js";
|
|
12
|
+
export const SOLUTIONS_DIR = "solutions";
|
|
13
|
+
/** The solution directory a file sits directly in, if any (SOLDOC-8.1). */
|
|
14
|
+
export function solutionDirOf(filePath) {
|
|
15
|
+
const parts = filePath.split(/[\\/]/);
|
|
16
|
+
const fileName = parts.at(-1);
|
|
17
|
+
const solutionSlug = parts.at(-2);
|
|
18
|
+
if (!fileName ||
|
|
19
|
+
!solutionSlug ||
|
|
20
|
+
parts.at(-3) !== SOLUTIONS_DIR ||
|
|
21
|
+
parts.at(-4) !== ".requirements" ||
|
|
22
|
+
!fileName.endsWith(".md")) {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
return { solutionSlug, outcomeSlug: stemOf(fileName) };
|
|
26
|
+
}
|
|
27
|
+
/** `pitch` for `pitch.md` and for `pitch.requirements.md`. */
|
|
28
|
+
export function stemOf(fileName) {
|
|
29
|
+
return fileName.replace(/\.requirements\.md$/, "").replace(/\.md$/, "");
|
|
30
|
+
}
|
|
31
|
+
/** SOLDOC-3: the name, never the content. Undefined for a file the rule doesn't reach. */
|
|
32
|
+
export function requirementsDocByName(filePath) {
|
|
33
|
+
if (!solutionDirOf(filePath))
|
|
34
|
+
return undefined;
|
|
35
|
+
return filePath.endsWith(".requirements.md");
|
|
36
|
+
}
|
|
37
|
+
/** The directory a solution's documents live in, by name. */
|
|
38
|
+
export function directoryOf(solution) {
|
|
39
|
+
return solution.directory ?? sanitizeFileName(solution.title);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* What a pushed file belongs to (SOLDOC-4, SOLDOC-5): the frontmatter's
|
|
43
|
+
* belonging when it has one, else what its path claims, resolved against
|
|
44
|
+
* the project's solutions. A frontmatter that disagrees with the path is
|
|
45
|
+
* kept and reported; a path that resolves to nothing refuses the push.
|
|
46
|
+
*/
|
|
47
|
+
export function resolveBelonging(filePath, frontmatter, claim, solutions) {
|
|
48
|
+
const declared = frontmatter.solution && frontmatter.outcome
|
|
49
|
+
? { solutionId: frontmatter.solution, outcome: frontmatter.outcome }
|
|
50
|
+
: undefined;
|
|
51
|
+
if (!claim)
|
|
52
|
+
return { belonging: declared };
|
|
53
|
+
const name = path.basename(filePath);
|
|
54
|
+
if (declared) {
|
|
55
|
+
// SOLDOC-5: the frontmatter wins; the path is layout. Say when they part.
|
|
56
|
+
const owner = solutions?.find((s) => s.solutionId === declared.solutionId);
|
|
57
|
+
if (owner && directoryOf(owner) !== claim.solutionSlug) {
|
|
58
|
+
return {
|
|
59
|
+
belonging: declared,
|
|
60
|
+
warning: `${name}: its path names a different solution than its frontmatter ("${owner.title}"); the frontmatter's belonging is kept.`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
if (sanitizeFileName(declared.outcome) !== claim.outcomeSlug) {
|
|
64
|
+
return {
|
|
65
|
+
belonging: declared,
|
|
66
|
+
warning: `${name}: its filename names a different outcome than its frontmatter ("${declared.outcome}"); the frontmatter's belonging is kept.`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return { belonging: declared };
|
|
70
|
+
}
|
|
71
|
+
// SOLDOC-4, SOLDOC-9: a new file's path is its declaration, read against
|
|
72
|
+
// each solution's own directory name.
|
|
73
|
+
const matches = (solutions ?? []).filter((s) => directoryOf(s) === claim.solutionSlug);
|
|
74
|
+
if (matches.length === 0) {
|
|
75
|
+
throw new Error(`${name}: its directory "${claim.solutionSlug}" matches no solution in the project, so it can't be placed. Name the directory as the solution's is named: its title as a slug, with its numeric suffix when an older solution has the same title.`);
|
|
76
|
+
}
|
|
77
|
+
if (matches.length > 1) {
|
|
78
|
+
throw new Error(`${name}: its directory "${claim.solutionSlug}" matches more than one solution (${matches.map((s) => `"${s.title}"`).join(", ")}), so it can't be placed.`);
|
|
79
|
+
}
|
|
80
|
+
const [solution] = matches;
|
|
81
|
+
const outcome = solution.outcomes.find((o) => sanitizeFileName(o) === claim.outcomeSlug);
|
|
82
|
+
if (!outcome) {
|
|
83
|
+
throw new Error(`${name}: "${claim.outcomeSlug}" matches no outcome of "${solution.title}". Its plan produces: ${solution.outcomes.map((o) => `${o} (${sanitizeFileName(o)}.md)`).join(", ")}.`);
|
|
84
|
+
}
|
|
85
|
+
return { belonging: { solutionId: solution.solutionId, outcome } };
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* SOLDOC-6, SOLDOC-9: where a pulled document that belongs to a solution
|
|
89
|
+
* lands when it has no file yet: in the solution's own directory, named for
|
|
90
|
+
* its outcome, with the `.requirements.md` name when it is a requirements
|
|
91
|
+
* document.
|
|
92
|
+
*/
|
|
93
|
+
export function solutionDocumentPath(requirementsDir, solutionDirectory, outcome, isRequirementsDoc) {
|
|
94
|
+
const suffix = isRequirementsDoc ? ".requirements.md" : ".md";
|
|
95
|
+
return path.join(requirementsDir, SOLUTIONS_DIR, solutionDirectory || "solution", `${sanitizeFileName(outcome) || "outcome"}${suffix}`);
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=solution-documents.js.map
|
package/dist/sync/types.d.ts
CHANGED
|
@@ -29,12 +29,15 @@ export interface LocalDocument {
|
|
|
29
29
|
defaultPrefix?: string;
|
|
30
30
|
/** document.attachments from frontmatter (ATTACH-10). */
|
|
31
31
|
attachments?: Attachment[];
|
|
32
|
+
/** SOLDOC-5: the belonging the frontmatter declares. */
|
|
33
|
+
solution?: string;
|
|
34
|
+
outcome?: string;
|
|
32
35
|
/** Frontmatter-stripped markdown body (the comparison unit, SYNC-ARCH-1). */
|
|
33
36
|
body?: string;
|
|
34
37
|
parseError?: string;
|
|
35
38
|
}
|
|
36
39
|
/**
|
|
37
|
-
* One published document as it exists in the cloud, from
|
|
40
|
+
* One published document as it exists in the cloud, from the project export.
|
|
38
41
|
*/
|
|
39
42
|
export interface CloudDocument {
|
|
40
43
|
documentId: string;
|
|
@@ -46,6 +49,16 @@ export interface CloudDocument {
|
|
|
46
49
|
body: string;
|
|
47
50
|
version: number;
|
|
48
51
|
updatedAt: number;
|
|
52
|
+
/** SOLDOC-6: which solution and outcome it belongs to, with the solution's title and directory name. */
|
|
53
|
+
solution?: {
|
|
54
|
+
solutionId: string;
|
|
55
|
+
outcome: string;
|
|
56
|
+
title: string | null;
|
|
57
|
+
/** SOLDOC-9: the solution's directory name; absent from an older backend. */
|
|
58
|
+
directory?: string | null;
|
|
59
|
+
};
|
|
60
|
+
/** SOLDOC-2/3: absent means a requirements document. */
|
|
61
|
+
isRequirementsDoc?: boolean;
|
|
49
62
|
}
|
|
50
63
|
export interface LocalSnapshot {
|
|
51
64
|
documents: LocalDocument[];
|
|
@@ -54,6 +67,11 @@ export interface CloudSnapshot {
|
|
|
54
67
|
projectName: string;
|
|
55
68
|
projectSlug: string;
|
|
56
69
|
documents: CloudDocument[];
|
|
70
|
+
/**
|
|
71
|
+
* SOLACCESS-2.2: whether the project's team has solutions, as the export
|
|
72
|
+
* read it; absent from an older backend, which reads as no.
|
|
73
|
+
*/
|
|
74
|
+
solutions?: boolean;
|
|
57
75
|
}
|
|
58
76
|
/**
|
|
59
77
|
* A single point of difference within a paired document, for verbose
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a failed command tells Sam (ERR-6).
|
|
3
|
+
*
|
|
4
|
+
* Production Convex replaces the message of every server error with
|
|
5
|
+
* "[Request ID: …] Server Error" and keeps only a ConvexError's `data`, which
|
|
6
|
+
* is where the backend puts the sentence written for users. So a refusal is
|
|
7
|
+
* read from its data; a server failure that carries none becomes a fixed
|
|
8
|
+
* sentence with its request ID; anything else is an error raised on Sam's
|
|
9
|
+
* machine, whose own message is already meant to be read.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* The user-facing message a ConvexError's data carries, if any: the data is
|
|
13
|
+
* either a plain string or an object with a `message`. Mirrors
|
|
14
|
+
* `packages/web/lib/convex-error.ts`.
|
|
15
|
+
*/
|
|
16
|
+
export declare function userFacingMessage(data: unknown): string | undefined;
|
|
17
|
+
/** The text to print for an error that ended a command. */
|
|
18
|
+
export declare function commandErrorMessage(error: unknown): string;
|
|
19
|
+
//# sourceMappingURL=command-error.d.ts.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a failed command tells Sam (ERR-6).
|
|
3
|
+
*
|
|
4
|
+
* Production Convex replaces the message of every server error with
|
|
5
|
+
* "[Request ID: …] Server Error" and keeps only a ConvexError's `data`, which
|
|
6
|
+
* is where the backend puts the sentence written for users. So a refusal is
|
|
7
|
+
* read from its data; a server failure that carries none becomes a fixed
|
|
8
|
+
* sentence with its request ID; anything else is an error raised on Sam's
|
|
9
|
+
* machine, whose own message is already meant to be read.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* The user-facing message a ConvexError's data carries, if any: the data is
|
|
13
|
+
* either a plain string or an object with a `message`. Mirrors
|
|
14
|
+
* `packages/web/lib/convex-error.ts`.
|
|
15
|
+
*/
|
|
16
|
+
export function userFacingMessage(data) {
|
|
17
|
+
if (typeof data === "string" && data.trim())
|
|
18
|
+
return data;
|
|
19
|
+
if (data && typeof data === "object" && "message" in data) {
|
|
20
|
+
const { message } = data;
|
|
21
|
+
if (typeof message === "string" && message.trim())
|
|
22
|
+
return message;
|
|
23
|
+
}
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
const SERVER_ERROR = /\bServer Error\b/;
|
|
27
|
+
const REQUEST_ID = /Request ID: ([\w-]+)/;
|
|
28
|
+
/** The text to print for an error that ended a command. */
|
|
29
|
+
export function commandErrorMessage(error) {
|
|
30
|
+
if (error && typeof error === "object" && "data" in error) {
|
|
31
|
+
const message = userFacingMessage(error.data);
|
|
32
|
+
if (message)
|
|
33
|
+
return message;
|
|
34
|
+
}
|
|
35
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
36
|
+
if (SERVER_ERROR.test(raw)) {
|
|
37
|
+
const requestId = REQUEST_ID.exec(raw)?.[1];
|
|
38
|
+
return requestId
|
|
39
|
+
? `Something went wrong in dotrequirements' cloud (request ID ${requestId}). Try again, and if it keeps happening, email support@dotrequirements.io with the request ID.`
|
|
40
|
+
: "Something went wrong in dotrequirements' cloud. Try again, and if it keeps happening, email support@dotrequirements.io.";
|
|
41
|
+
}
|
|
42
|
+
return raw;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=command-error.js.map
|
package/dist/utils/oauth-flow.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { randomBytes } from "node:crypto";
|
|
8
8
|
import { ConvexHttpClient } from "convex/browser";
|
|
9
|
-
import {
|
|
9
|
+
import { convexUrl } from "../cloud.js";
|
|
10
10
|
import { api } from "../convex.js";
|
|
11
11
|
import { openBrowserWithFallback } from "./browser-launch.js";
|
|
12
12
|
import { startCallbackServer } from "./oauth-callback-server.js";
|
|
@@ -58,7 +58,7 @@ export async function executeOAuthFlow() {
|
|
|
58
58
|
}
|
|
59
59
|
console.log("Authentication successful, exchanging code for tokens...");
|
|
60
60
|
// Exchange authorization code for tokens via Convex action (server-side WorkOS call)
|
|
61
|
-
const client = new ConvexHttpClient(
|
|
61
|
+
const client = new ConvexHttpClient(convexUrl());
|
|
62
62
|
const result = await client.action(api.lib.authActions.exchangeAuthorizationCode, {
|
|
63
63
|
code,
|
|
64
64
|
codeVerifier,
|
|
@@ -43,9 +43,9 @@ export async function selectTeam(client) {
|
|
|
43
43
|
*/
|
|
44
44
|
export async function selectProject(client, teamId) {
|
|
45
45
|
console.log("\nFetching projects...\n");
|
|
46
|
-
const projectsWithSecrets = (await client.query(api.projectSecrets.queries.
|
|
46
|
+
const projectsWithSecrets = (await client.query(api.projectSecrets.queries.listOwnForTeam,
|
|
47
47
|
// biome-ignore lint/suspicious/noExplicitAny: CLI workspace doesn't import convex's branded Id<"teams"> type; the runtime value is a plain string the convex server validates.
|
|
48
|
-
{
|
|
48
|
+
{ teamId: teamId }));
|
|
49
49
|
if (projectsWithSecrets.length === 0) {
|
|
50
50
|
console.log("No projects found in this team.");
|
|
51
51
|
console.log('Use "dotrequirements init" to create a new project.\n');
|
|
@@ -79,9 +79,9 @@ export async function selectProject(client, teamId) {
|
|
|
79
79
|
* Free tier always gets 30 days regardless of this parameter.
|
|
80
80
|
*/
|
|
81
81
|
export async function getOrCreateProjectSecret(client, projectId, expiryDays) {
|
|
82
|
-
const result = (await client.mutation(api.projectSecrets.mutations.
|
|
82
|
+
const result = (await client.mutation(api.projectSecrets.mutations.ensureOwn, {
|
|
83
83
|
// biome-ignore lint/suspicious/noExplicitAny: CLI workspace doesn't import convex's branded Id<"projects"> type; the runtime value is a plain string the convex server validates.
|
|
84
|
-
|
|
84
|
+
projectId: projectId,
|
|
85
85
|
expiryDays,
|
|
86
86
|
}));
|
|
87
87
|
return result;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the cloud has to say to this CLI version (CLI-MSG-1, CLI-MSG-3).
|
|
3
|
+
*
|
|
4
|
+
* Once enabled, the first cloud use in a run asks the cloud for this
|
|
5
|
+
* version's messages, alongside the command's own work. At the end the
|
|
6
|
+
* command takes them: text commands print them after their output, JSON
|
|
7
|
+
* commands carry them in their JSON. Asking never changes the command: a
|
|
8
|
+
* failed, unreadable or slow answer means no messages.
|
|
9
|
+
*/
|
|
10
|
+
/** How long a finished command waits for the cloud's answer (CLI-MSG-3.2). */
|
|
11
|
+
export declare const MESSAGE_WAIT_MS = 1000;
|
|
12
|
+
/** Ask the cloud for this version's messages on the run's first cloud use. */
|
|
13
|
+
export declare function enableVersionMessages(): void;
|
|
14
|
+
/**
|
|
15
|
+
* The messages for this version, waiting at most `waitMs` for the cloud's
|
|
16
|
+
* answer; empty when the run never reached the cloud or they were already
|
|
17
|
+
* shown. Asking twice waits once.
|
|
18
|
+
*/
|
|
19
|
+
export declare function versionMessages(waitMs?: number): Promise<string[]>;
|
|
20
|
+
/**
|
|
21
|
+
* Record that this run's messages reached the user, once they are actually
|
|
22
|
+
* printed, so `wrapCommand` doesn't print them again after a JSON command
|
|
23
|
+
* carried them — and does print them if that command failed first.
|
|
24
|
+
*/
|
|
25
|
+
export declare function markVersionMessagesShown(): void;
|
|
26
|
+
/** The messages as text, for a command's human-readable output. */
|
|
27
|
+
export declare function formatVersionMessages(messages: string[]): string;
|
|
28
|
+
//# sourceMappingURL=version-messages.d.ts.map
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the cloud has to say to this CLI version (CLI-MSG-1, CLI-MSG-3).
|
|
3
|
+
*
|
|
4
|
+
* Once enabled, the first cloud use in a run asks the cloud for this
|
|
5
|
+
* version's messages, alongside the command's own work. At the end the
|
|
6
|
+
* command takes them: text commands print them after their output, JSON
|
|
7
|
+
* commands carry them in their JSON. Asking never changes the command: a
|
|
8
|
+
* failed, unreadable or slow answer means no messages.
|
|
9
|
+
*/
|
|
10
|
+
import { convexUrl, whenCloudFirstUsed } from "./cloud.js";
|
|
11
|
+
import { getOwnPackage } from "./utils/own-package.js";
|
|
12
|
+
/** How long a finished command waits for the cloud's answer (CLI-MSG-3.2). */
|
|
13
|
+
export const MESSAGE_WAIT_MS = 1_000;
|
|
14
|
+
let pending = null;
|
|
15
|
+
let abort = null;
|
|
16
|
+
let shown = false;
|
|
17
|
+
async function askCloud(signal) {
|
|
18
|
+
try {
|
|
19
|
+
const response = await fetch(`${convexUrl()}/api/query`, {
|
|
20
|
+
method: "POST",
|
|
21
|
+
headers: { "Content-Type": "application/json" },
|
|
22
|
+
body: JSON.stringify({
|
|
23
|
+
path: "cliMessages/queries:forVersion",
|
|
24
|
+
args: { version: getOwnPackage().version },
|
|
25
|
+
format: "json",
|
|
26
|
+
}),
|
|
27
|
+
signal,
|
|
28
|
+
});
|
|
29
|
+
if (!response.ok)
|
|
30
|
+
return [];
|
|
31
|
+
const body = (await response.json());
|
|
32
|
+
const messages = body.status === "success" ? body.value?.messages : null;
|
|
33
|
+
return Array.isArray(messages)
|
|
34
|
+
? messages.filter((m) => typeof m === "string" && !!m)
|
|
35
|
+
: [];
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** Ask the cloud for this version's messages on the run's first cloud use. */
|
|
42
|
+
export function enableVersionMessages() {
|
|
43
|
+
whenCloudFirstUsed(() => {
|
|
44
|
+
if (pending)
|
|
45
|
+
return;
|
|
46
|
+
const controller = new AbortController();
|
|
47
|
+
abort = controller;
|
|
48
|
+
// Asked just after the command's own first request goes out, so the
|
|
49
|
+
// command's work is never queued behind the question.
|
|
50
|
+
pending = new Promise((resolve) => {
|
|
51
|
+
queueMicrotask(() => resolve(askCloud(controller.signal)));
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The messages for this version, waiting at most `waitMs` for the cloud's
|
|
57
|
+
* answer; empty when the run never reached the cloud or they were already
|
|
58
|
+
* shown. Asking twice waits once.
|
|
59
|
+
*/
|
|
60
|
+
export async function versionMessages(waitMs = MESSAGE_WAIT_MS) {
|
|
61
|
+
if (shown || !pending)
|
|
62
|
+
return [];
|
|
63
|
+
let timer;
|
|
64
|
+
const late = new Promise((resolve) => {
|
|
65
|
+
timer = setTimeout(() => resolve([]), waitMs);
|
|
66
|
+
});
|
|
67
|
+
const messages = await Promise.race([pending, late]);
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
// Whatever came back is the answer: a slow one must neither keep the
|
|
70
|
+
// process alive after the command nor be waited for a second time.
|
|
71
|
+
abort?.abort();
|
|
72
|
+
pending = Promise.resolve(messages);
|
|
73
|
+
return messages;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Record that this run's messages reached the user, once they are actually
|
|
77
|
+
* printed, so `wrapCommand` doesn't print them again after a JSON command
|
|
78
|
+
* carried them — and does print them if that command failed first.
|
|
79
|
+
*/
|
|
80
|
+
export function markVersionMessagesShown() {
|
|
81
|
+
shown = true;
|
|
82
|
+
}
|
|
83
|
+
/** The messages as text, for a command's human-readable output. */
|
|
84
|
+
export function formatVersionMessages(messages) {
|
|
85
|
+
return messages.map((message) => `\n${message}`).join("\n");
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=version-messages.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@popoverai/dotrequirements",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.0",
|
|
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": {
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"./schema": "./dist/schema/index.js",
|
|
14
14
|
"./schema/browser": "./dist/schema/browser.js",
|
|
15
15
|
"./style-guide": "./dist/requirements/style-guide.js",
|
|
16
|
-
"./greenfield": "./dist/requirements/greenfield.js"
|
|
16
|
+
"./greenfield": "./dist/requirements/greenfield.js",
|
|
17
|
+
"./rehearsal": "./dist/rehearsal/index.js"
|
|
17
18
|
},
|
|
18
19
|
"publishConfig": {
|
|
19
20
|
"access": "public"
|
package/dist/config.d.ts
DELETED
package/dist/config.js
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Production Convex deployment URL
|
|
3
|
-
* All users connect to the same backend
|
|
4
|
-
*/
|
|
5
|
-
const CONVEX_URL = "https://data.dotrequirements.io";
|
|
6
|
-
/**
|
|
7
|
-
* Get the Convex deployment URL
|
|
8
|
-
*/
|
|
9
|
-
export function getConvexUrl() {
|
|
10
|
-
return CONVEX_URL;
|
|
11
|
-
}
|
|
12
|
-
//# sourceMappingURL=config.js.map
|