@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.
Files changed (135) hide show
  1. package/dist/cli.js +32 -7
  2. package/dist/cloud.d.ts +18 -0
  3. package/dist/cloud.js +38 -0
  4. package/dist/commands/board/index.d.ts +13 -0
  5. package/dist/commands/board/index.js +96 -0
  6. package/dist/commands/branch-merge.d.ts +78 -0
  7. package/dist/commands/branch-merge.js +129 -0
  8. package/dist/commands/create-requirement-document.js +2 -2
  9. package/dist/commands/diff.js +15 -5
  10. package/dist/commands/init.js +7 -6
  11. package/dist/commands/link.js +27 -21
  12. package/dist/commands/mockup/index.d.ts +11 -0
  13. package/dist/commands/mockup/index.js +82 -0
  14. package/dist/commands/report.js +22 -9
  15. package/dist/commands/review-test.js +3 -2
  16. package/dist/commands/scores/index.d.ts +50 -0
  17. package/dist/commands/scores/index.js +190 -0
  18. package/dist/commands/solution/ask.d.ts +9 -0
  19. package/dist/commands/solution/ask.js +27 -0
  20. package/dist/commands/solution/author.d.ts +19 -0
  21. package/dist/commands/solution/author.js +202 -0
  22. package/dist/commands/solution/branch.d.ts +103 -0
  23. package/dist/commands/solution/branch.js +341 -0
  24. package/dist/commands/solution/brief.d.ts +81 -0
  25. package/dist/commands/solution/brief.js +47 -0
  26. package/dist/commands/solution/checkout.d.ts +27 -0
  27. package/dist/commands/solution/checkout.js +66 -0
  28. package/dist/commands/solution/client.d.ts +16 -0
  29. package/dist/commands/solution/client.js +46 -0
  30. package/dist/commands/solution/contract.d.ts +9 -0
  31. package/dist/commands/solution/contract.js +23 -0
  32. package/dist/commands/solution/create.d.ts +14 -0
  33. package/dist/commands/solution/create.js +52 -0
  34. package/dist/commands/solution/demo-script.d.ts +28 -0
  35. package/dist/commands/solution/demo-script.js +43 -0
  36. package/dist/commands/solution/image.d.ts +60 -0
  37. package/dist/commands/solution/image.js +148 -0
  38. package/dist/commands/solution/index.d.ts +12 -0
  39. package/dist/commands/solution/index.js +281 -0
  40. package/dist/commands/solution/look-up.d.ts +9 -0
  41. package/dist/commands/solution/look-up.js +69 -0
  42. package/dist/commands/solution/next.d.ts +12 -0
  43. package/dist/commands/solution/next.js +48 -0
  44. package/dist/commands/solution/plan-file.d.ts +22 -0
  45. package/dist/commands/solution/plan-file.js +33 -0
  46. package/dist/commands/solution/preview-screenshot.d.ts +21 -0
  47. package/dist/commands/solution/preview-screenshot.js +56 -0
  48. package/dist/commands/solution/pull-request.d.ts +25 -0
  49. package/dist/commands/solution/pull-request.js +117 -0
  50. package/dist/commands/solution/pushing.d.ts +66 -0
  51. package/dist/commands/solution/pushing.js +185 -0
  52. package/dist/commands/solution/rehearse.d.ts +10 -0
  53. package/dist/commands/solution/rehearse.js +24 -0
  54. package/dist/commands/solution/request.d.ts +3 -0
  55. package/dist/commands/solution/request.js +13 -0
  56. package/dist/commands/solution/review.d.ts +27 -0
  57. package/dist/commands/solution/review.js +104 -0
  58. package/dist/commands/solution/run-file.d.ts +42 -0
  59. package/dist/commands/solution/run-file.js +63 -0
  60. package/dist/commands/solution/share.d.ts +62 -0
  61. package/dist/commands/solution/share.js +346 -0
  62. package/dist/commands/solution/stall.d.ts +14 -0
  63. package/dist/commands/solution/stall.js +41 -0
  64. package/dist/commands/solution/start.d.ts +11 -0
  65. package/dist/commands/solution/start.js +20 -0
  66. package/dist/commands/solution/status.d.ts +39 -0
  67. package/dist/commands/solution/status.js +145 -0
  68. package/dist/commands/solution/summary.d.ts +13 -0
  69. package/dist/commands/solution/summary.js +5 -0
  70. package/dist/commands/solution/things.d.ts +118 -0
  71. package/dist/commands/solution/things.js +78 -0
  72. package/dist/commands/solution/thread.d.ts +13 -0
  73. package/dist/commands/solution/thread.js +53 -0
  74. package/dist/commands/solution/upload.d.ts +24 -0
  75. package/dist/commands/solution/upload.js +27 -0
  76. package/dist/commands/solution/video.d.ts +88 -0
  77. package/dist/commands/solution/video.js +364 -0
  78. package/dist/commands/solution/working-folder.d.ts +44 -0
  79. package/dist/commands/solution/working-folder.js +64 -0
  80. package/dist/commands/style-check.js +4 -4
  81. package/dist/commands/sync.d.ts +2 -1
  82. package/dist/commands/sync.js +21 -5
  83. package/dist/convex.d.ts +64 -10
  84. package/dist/convex.js +75 -10
  85. package/dist/harness/finalize.js +16 -8
  86. package/dist/harness/reportingStatus.js +2 -11
  87. package/dist/push/core.js +7 -2
  88. package/dist/rehearsal/derive.d.ts +23 -0
  89. package/dist/rehearsal/derive.js +112 -0
  90. package/dist/rehearsal/driver.d.ts +44 -0
  91. package/dist/rehearsal/driver.js +337 -0
  92. package/dist/rehearsal/earlier.d.ts +25 -0
  93. package/dist/rehearsal/earlier.js +138 -0
  94. package/dist/rehearsal/index.d.ts +5 -0
  95. package/dist/rehearsal/index.js +4 -0
  96. package/dist/rehearsal/live.d.ts +72 -0
  97. package/dist/rehearsal/live.js +301 -0
  98. package/dist/rehearsal/report.d.ts +4 -0
  99. package/dist/rehearsal/report.js +69 -0
  100. package/dist/rehearsal/types.d.ts +377 -0
  101. package/dist/rehearsal/types.js +8 -0
  102. package/dist/requirements/cloud-ai.d.ts +0 -6
  103. package/dist/requirements/cloud-ai.js +2 -11
  104. package/dist/requirements/cloud-coverage.js +4 -4
  105. package/dist/requirements/index.d.ts +6 -0
  106. package/dist/requirements/index.js +24 -0
  107. package/dist/schema/browser.d.ts +1 -1
  108. package/dist/schema/browser.js +3 -1
  109. package/dist/schema/file-writer.d.ts +5 -0
  110. package/dist/schema/file-writer.js +4 -0
  111. package/dist/schema/parser.d.ts +11 -2
  112. package/dist/schema/parser.js +11 -7
  113. package/dist/schema/schemas.d.ts +40 -0
  114. package/dist/schema/schemas.js +4 -0
  115. package/dist/sync/execute.d.ts +10 -1
  116. package/dist/sync/execute.js +76 -24
  117. package/dist/sync/index.d.ts +1 -1
  118. package/dist/sync/index.js +1 -1
  119. package/dist/sync/publish.d.ts +7 -0
  120. package/dist/sync/publish.js +2 -2
  121. package/dist/sync/segment.js +4 -1
  122. package/dist/sync/snapshot.d.ts +27 -5
  123. package/dist/sync/snapshot.js +92 -15
  124. package/dist/sync/solution-documents.d.ts +64 -0
  125. package/dist/sync/solution-documents.js +97 -0
  126. package/dist/sync/types.d.ts +19 -1
  127. package/dist/utils/command-error.d.ts +19 -0
  128. package/dist/utils/command-error.js +44 -0
  129. package/dist/utils/oauth-flow.js +2 -2
  130. package/dist/utils/project-selector.js +4 -4
  131. package/dist/version-messages.d.ts +28 -0
  132. package/dist/version-messages.js +87 -0
  133. package/package.json +3 -2
  134. package/dist/config.d.ts +0 -5
  135. package/dist/config.js +0 -12
@@ -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 exportProjectForCli query.
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 { getConvexUrl } from "../config.js";
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
- const paths = filePaths ?? (await findRequirementsFiles(workspaceRoot));
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
- * Build the cloud snapshot with one exportProjectForCli query. A share token is
124
- * passed as the projectSecret with no slug (the read-only wrapper resolves the
125
- * project from the token) — the read-only path never writes back.
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(getConvexUrl());
129
- const projectAuth = auth.projectSlug
130
- ? { projectSlug: auth.projectSlug, projectSecret: auth.projectSecret }
131
- : { projectSecret: auth.projectSecret };
132
- const result = (await client.query(api.documents.queries.exportProjectForCli, {
133
- projectAuth,
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
@@ -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 exportProjectForCli.
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
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import { randomBytes } from "node:crypto";
8
8
  import { ConvexHttpClient } from "convex/browser";
9
- import { getConvexUrl } from "../config.js";
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(getConvexUrl());
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.listOwnSecretsForTeam,
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
- { target: { type: "team", id: teamId } }));
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.ensureOwnSecret, {
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
- target: { type: "project", id: projectId },
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.32.1",
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
@@ -1,5 +0,0 @@
1
- /**
2
- * Get the Convex deployment URL
3
- */
4
- export declare function getConvexUrl(): string;
5
- //# sourceMappingURL=config.d.ts.map
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