@popoverai/dotrequirements 0.27.4 → 0.28.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 (50) hide show
  1. package/README.md +24 -20
  2. package/dist/cli.js +34 -12
  3. package/dist/commands/aliases.d.ts +26 -0
  4. package/dist/commands/aliases.js +31 -0
  5. package/dist/commands/diff.d.ts +14 -0
  6. package/dist/commands/diff.js +62 -0
  7. package/dist/commands/init.js +5 -5
  8. package/dist/commands/link.d.ts +1 -1
  9. package/dist/commands/link.js +10 -7
  10. package/dist/commands/sync-common.d.ts +21 -0
  11. package/dist/commands/sync-common.js +24 -0
  12. package/dist/commands/sync.d.ts +26 -0
  13. package/dist/commands/sync.js +182 -0
  14. package/dist/convex.d.ts +1 -1
  15. package/dist/convex.js +2 -2
  16. package/dist/push/core.d.ts +18 -116
  17. package/dist/push/core.js +16 -267
  18. package/dist/push/index.d.ts +3 -3
  19. package/dist/push/index.js +4 -4
  20. package/dist/requirements/style-guide.js +3 -3
  21. package/dist/schema/schemas.js +1 -1
  22. package/dist/sync/compare.d.ts +21 -0
  23. package/dist/sync/compare.js +285 -0
  24. package/dist/sync/execute.d.ts +48 -0
  25. package/dist/sync/execute.js +186 -0
  26. package/dist/sync/index.d.ts +21 -0
  27. package/dist/sync/index.js +52 -0
  28. package/dist/sync/local-files.d.ts +26 -0
  29. package/dist/sync/local-files.js +91 -0
  30. package/dist/sync/plan.d.ts +39 -0
  31. package/dist/sync/plan.js +90 -0
  32. package/dist/sync/render.d.ts +17 -0
  33. package/dist/sync/render.js +46 -0
  34. package/dist/sync/segment.d.ts +40 -0
  35. package/dist/sync/segment.js +76 -0
  36. package/dist/sync/snapshot.d.ts +30 -0
  37. package/dist/sync/snapshot.js +118 -0
  38. package/dist/sync/types.d.ts +82 -0
  39. package/dist/sync/types.js +12 -0
  40. package/dist/templates/context-file-section.md +2 -1
  41. package/dist/templates/requirements-readme.js +3 -4
  42. package/dist/templates/requirements-readme.ts +3 -4
  43. package/dist/templates/skills/codebase-to-spec/SKILL.md +2 -2
  44. package/dist/utils/project-settings.d.ts +8 -2
  45. package/dist/utils/project-settings.js +47 -24
  46. package/package.json +1 -1
  47. package/dist/commands/pull.d.ts +0 -8
  48. package/dist/commands/pull.js +0 -230
  49. package/dist/commands/push.d.ts +0 -6
  50. package/dist/commands/push.js +0 -244
package/dist/push/core.js CHANGED
@@ -1,20 +1,12 @@
1
1
  /**
2
- * Shared push logic for CLI and MCP.
3
- *
4
- * This module contains the core push functionality that both the CLI push command
5
- * and MCP push_requirements tool use. It handles:
6
- * - Parsing local files
7
- * - Dry run validation with Convex
8
- * - Conflict detection
9
- * - Executing the push
10
- * - Updating local files with document IDs and timestamps
2
+ * Shared parse-and-write-back primitives for syncing local requirements files
3
+ * to the cloud. The comparator-driven sync path (`packages/cli/src/sync/`)
4
+ * consumes these; the former dry-run/execute push pipeline was retired with the
5
+ * `push` command (its behavior now lives in the sync executor).
11
6
  */
12
7
  import * as fs from "node:fs";
13
- import * as path from "node:path";
14
- import { ConvexHttpClient } from "convex/browser";
15
8
  import YAML from "yaml";
16
- import { api } from "../convex.js";
17
- import { buildRequirementsFile, getAllRequirements, parseRequirementKey, parseRequirementsFromFile, } from "../schema/index.js";
9
+ import { getAllRequirements, parseRequirementKey, parseRequirementsFromFile, } from "../schema/index.js";
18
10
  import { extractFrontmatterBlock, stripFrontmatterBlock, } from "../schema/parser-core.js";
19
11
  /** Base URL of the web app, where synced documents are reviewed. */
20
12
  export const WEB_APP_URL = "https://app.dotrequirements.io";
@@ -26,7 +18,7 @@ export const WEB_APP_URL = "https://app.dotrequirements.io";
26
18
  */
27
19
  export function extractMarkdownContent(rawContent) {
28
20
  // Shared CRLF-normalizing helper (SYNC-FORMAT-1): the stripped body is
29
- // pushed as the document's canonical markdownContent (SYNC-ARCH-1), so a
21
+ // synced as the document's canonical markdownContent (SYNC-ARCH-1), so a
30
22
  // CRLF-authored file must not leak its frontmatter fence into the cloud
31
23
  // body.
32
24
  return stripFrontmatterBlock(rawContent);
@@ -55,11 +47,11 @@ function extractRawFrontmatter(rawContent) {
55
47
  return undefined;
56
48
  }
57
49
  /**
58
- * DOC-HEADER-14: merge the validated (and push-updated) metadata over the raw
50
+ * DOC-HEADER-14: merge the validated (and sync-updated) metadata over the raw
59
51
  * frontmatter so unrecognized keys survive the rewrite while the fields the
60
- * push owns (document ID, pulledAt, defaultPrefix, version) stay updated.
52
+ * sync owns (document ID, pulledAt, defaultPrefix, version) stay updated.
61
53
  */
62
- function mergeMetadataWithRawFrontmatter(metadata, rawFrontmatter) {
54
+ export function mergeMetadataWithRawFrontmatter(metadata, rawFrontmatter) {
63
55
  if (!rawFrontmatter) {
64
56
  return metadata;
65
57
  }
@@ -77,11 +69,11 @@ function mergeMetadataWithRawFrontmatter(metadata, rawFrontmatter) {
77
69
  return merged;
78
70
  }
79
71
  /**
80
- * Parse files for push. Returns parsed files with metadata and content.
72
+ * Parse files for sync. Returns parsed files with metadata and content.
81
73
  *
82
74
  * Throws (via `parseRequirementsFromFile`) if any file is syntactically
83
75
  * invalid. Callers that need one bad file not to abort the batch should parse
84
- * files individually and collect failures (see the MCP push handler, #45).
76
+ * files individually and collect failures.
85
77
  */
86
78
  export function parseFilesForPush(filePaths) {
87
79
  const parsedFiles = [];
@@ -128,11 +120,11 @@ export function parseFilesForPush(filePaths) {
128
120
  /**
129
121
  * SYNC-FAIL-4.0: parse files one at a time so a single invalid file cannot
130
122
  * abort the batch — its failure is collected per file while the valid files
131
- * still parse. Shared by the CLI push command and the MCP push handler (#45)
132
- * so their isolation semantics cannot drift.
123
+ * still parse.
133
124
  *
134
125
  * Note: cross-file checks that need the whole batch (e.g. SYNC-FAIL-3
135
- * duplicate document IDs) are enforced downstream in dryRunPush, not here.
126
+ * duplicate document IDs) are enforced by the sync path against the local
127
+ * snapshot, not here.
136
128
  */
137
129
  export function parseFilesForPushIndividually(filePaths) {
138
130
  const parsedFiles = [];
@@ -154,7 +146,7 @@ export function parseFilesForPushIndividually(filePaths) {
154
146
  return { parsedFiles, totalRequirements, parseFailures };
155
147
  }
156
148
  /**
157
- * SYNC-FAIL-3: two ParsedFiles carrying the same document.id would both push
149
+ * SYNC-FAIL-3: two ParsedFiles carrying the same document.id would both sync
158
150
  * as updates to one cloud document — last writer wins and the first spec is
159
151
  * silently destroyed. Abort instead, naming both files and the shared id.
160
152
  */
@@ -170,252 +162,9 @@ export function assertNoDuplicateDocumentIds(parsedFiles) {
170
162
  ` ${existingPath}\n` +
171
163
  ` ${file.filePath}\n` +
172
164
  `Each file must map to its own cloud document. Remove the "id:" line ` +
173
- `from the copy's frontmatter so it pushes as a new document, then push again.`);
165
+ `from the copy's frontmatter so it syncs as a new document, then sync again.`);
174
166
  }
175
167
  filesByDocId.set(docId, file.filePath);
176
168
  }
177
169
  }
178
- // ============================================================================
179
- // Dry Run
180
- // ============================================================================
181
- /**
182
- * Execute dry run phase: validate all files against Convex and detect conflicts.
183
- */
184
- export async function dryRunPush(parsedFiles, credentials) {
185
- // SYNC-FAIL-3.0: callers that assemble ParsedFiles themselves (e.g. the
186
- // MCP handler parses files one at a time) still abort before any cloud write
187
- assertNoDuplicateDocumentIds(parsedFiles);
188
- const client = new ConvexHttpClient(credentials.convexUrl);
189
- const dryRunResults = [];
190
- // Phase 1: Dry run to categorize all files
191
- for (const file of parsedFiles) {
192
- const doc = file.metadata.document;
193
- const fileName = path.basename(file.filePath);
194
- if (!doc) {
195
- // No document section - mark as invalid locally
196
- dryRunResults.push({
197
- file,
198
- result: {
199
- dryRun: true,
200
- action: "invalid",
201
- title: fileName,
202
- error: "Missing document section in frontmatter",
203
- },
204
- });
205
- continue;
206
- }
207
- try {
208
- const result = (await client.mutation(api.documents.saveWithRequirements.saveWithRequirements, {
209
- projectAuth: {
210
- projectSlug: credentials.projectId,
211
- projectSecret: credentials.projectSecret,
212
- },
213
- target: { type: "project", slug: credentials.projectId },
214
- documentId: doc.id,
215
- title: doc.title,
216
- markdownContent: file.markdownContent,
217
- defaultPrefix: doc.defaultPrefix,
218
- dryRun: true,
219
- }));
220
- dryRunResults.push({ file, result });
221
- }
222
- catch (err) {
223
- dryRunResults.push({
224
- file,
225
- result: {
226
- dryRun: true,
227
- action: "invalid",
228
- title: doc.title,
229
- error: err.message,
230
- },
231
- });
232
- }
233
- }
234
- // Categorize results
235
- const updates = dryRunResults.filter((r) => r.result.action === "update");
236
- const creates = dryRunResults.filter((r) => r.result.action === "create");
237
- const notFound = dryRunResults.filter((r) => r.result.action === "not_found");
238
- const invalid = dryRunResults.filter((r) => r.result.action === "invalid");
239
- // Check for conflicts on updates (cloud changed since last pull)
240
- const conflicts = [];
241
- if (updates.length > 0) {
242
- const docIds = updates.map((u) => u.result.documentId);
243
- const cloudMetadata = await client.query(api.documents.queries.getDocumentsMetadata, {
244
- projectAuth: {
245
- projectSlug: credentials.projectId,
246
- projectSecret: credentials.projectSecret,
247
- },
248
- target: { type: "project", slug: credentials.projectId },
249
- documentIds: docIds,
250
- });
251
- const cloudMetaByDocId = new Map(cloudMetadata.map((m) => [m.documentId, m]));
252
- for (const item of updates) {
253
- const docId = item.result.documentId;
254
- const cloudMeta = cloudMetaByDocId.get(docId);
255
- if (cloudMeta && item.file.metadata.pulledAt) {
256
- const pulledAtMs = new Date(item.file.metadata.pulledAt).getTime();
257
- if (cloudMeta.updatedAt > pulledAtMs) {
258
- conflicts.push({ item, cloudMeta });
259
- }
260
- }
261
- }
262
- }
263
- return {
264
- updates,
265
- creates,
266
- notFound,
267
- invalid,
268
- conflicts,
269
- totalRequirements: parsedFiles.reduce((sum, f) => sum + f.requirementCount, 0),
270
- };
271
- }
272
- // ============================================================================
273
- // Execute Push
274
- // ============================================================================
275
- /**
276
- * Execute the push: save all pushable files to Convex and update local files.
277
- */
278
- export async function executePush(dryRunResult, credentials) {
279
- const client = new ConvexHttpClient(credentials.convexUrl);
280
- const pushableFiles = [
281
- ...dryRunResult.updates,
282
- ...dryRunResult.creates,
283
- ...dryRunResult.notFound,
284
- ];
285
- let created = 0;
286
- let updated = 0;
287
- const errors = [];
288
- const importWarnings = [];
289
- const synced = [];
290
- const writeBackWarnings = [];
291
- for (const { file, result } of pushableFiles) {
292
- const doc = file.metadata.document;
293
- const fileName = path.basename(file.filePath);
294
- // For not_found, clear the ID so we create a new document
295
- const effectiveDocId = result.action === "not_found" ? undefined : doc.id;
296
- try {
297
- const rawResult = (await client.mutation(api.documents.saveWithRequirements.saveWithRequirements, {
298
- projectAuth: {
299
- projectSlug: credentials.projectId,
300
- projectSecret: credentials.projectSecret,
301
- },
302
- target: { type: "project", slug: credentials.projectId },
303
- documentId: effectiveDocId,
304
- title: doc.title,
305
- markdownContent: file.markdownContent,
306
- defaultPrefix: doc.defaultPrefix,
307
- // IMPORT-2: forward the CTS run marker when the file is stamped
308
- runMarker: file.metadata.ctsRun,
309
- dryRun: false,
310
- }));
311
- // The server returns a structured result iff we sent a runMarker
312
- const pushResult = typeof rawResult === "string" ? rawResult : rawResult.documentId;
313
- if (typeof rawResult !== "string") {
314
- const status = rawResult.importStatus;
315
- if (status === "invalid_marker" ||
316
- status === "unsupported_version" ||
317
- status === "already_used") {
318
- // IMPORT-3.2/3.3: loud, never fatal
319
- importWarnings.push({ fileName, status });
320
- }
321
- }
322
- const isCreate = result.action === "create" || result.action === "not_found";
323
- if (isCreate) {
324
- // New document - write ID back to file
325
- doc.id = pushResult;
326
- file.metadata.pulledAt = new Date().toISOString();
327
- file.metadata.version = 1;
328
- created++;
329
- }
330
- else {
331
- // Update pulledAt to reflect this push
332
- file.metadata.pulledAt = new Date().toISOString();
333
- file.metadata.version = (file.metadata.version || 0) + 1;
334
- updated++;
335
- }
336
- // SYNC-FAIL-2: the local write-back is separate from the cloud save —
337
- // the cloud document already exists at this point, so a write-back
338
- // failure is a warning on a synced document, never a push failure
339
- // (which would invite a retry that mints a duplicate cloud document).
340
- try {
341
- // DOC-HEADER-14: unrecognized frontmatter keys survive the rewrite
342
- const updatedContent = buildRequirementsFile(mergeMetadataWithRawFrontmatter(file.metadata, file.rawFrontmatter), file.markdownContent);
343
- fs.writeFileSync(file.filePath, updatedContent, "utf-8");
344
- }
345
- catch (writeErr) {
346
- writeBackWarnings.push({
347
- fileName,
348
- filePath: file.filePath,
349
- documentId: pushResult,
350
- error: writeErr instanceof Error ? writeErr.message : String(writeErr),
351
- });
352
- }
353
- // SYNC-LAND-1: every synced document gets its web URL in the result
354
- synced.push({
355
- fileName,
356
- documentId: pushResult,
357
- url: `${WEB_APP_URL}/documents/${pushResult}`,
358
- });
359
- }
360
- catch (err) {
361
- errors.push({
362
- fileName,
363
- filePath: file.filePath,
364
- error: errorDisplayMessage(err),
365
- });
366
- }
367
- }
368
- return {
369
- created,
370
- updated,
371
- errors,
372
- importWarnings,
373
- synced,
374
- writeBackWarnings,
375
- };
376
- }
377
- /**
378
- * Servers throw ConvexError({kind, message}) for expected failures (e.g.
379
- * LIMITS-4.2's limit error); the client-side Error message embeds that data
380
- * as JSON. Surface the human-readable message it carries instead of the blob.
381
- */
382
- function errorDisplayMessage(err) {
383
- const data = err.data;
384
- const fromData = humanMessage(data);
385
- if (fromData)
386
- return fromData;
387
- const message = err instanceof Error ? err.message : String(err);
388
- // ConvexError messages may embed the data JSON directly or after a
389
- // "ConvexError:" prefix — try the trailing {...} chunk
390
- const jsonStart = message.indexOf("{");
391
- if (jsonStart !== -1) {
392
- try {
393
- const parsed = JSON.parse(message.slice(jsonStart));
394
- const fromMessage = humanMessage(parsed);
395
- if (fromMessage)
396
- return fromMessage;
397
- }
398
- catch {
399
- // fall through to the raw message
400
- }
401
- }
402
- return message;
403
- }
404
- function humanMessage(data) {
405
- if (typeof data === "string") {
406
- try {
407
- return humanMessage(JSON.parse(data));
408
- }
409
- catch {
410
- return undefined;
411
- }
412
- }
413
- if (data &&
414
- typeof data === "object" &&
415
- "message" in data &&
416
- typeof data.message === "string") {
417
- return data.message;
418
- }
419
- return undefined;
420
- }
421
170
  //# sourceMappingURL=core.js.map
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Push Module
3
3
  *
4
- * Shared push logic for syncing local requirements files to the cloud.
5
- * Used by both CLI push command and MCP push_requirements tool.
4
+ * Shared parse-and-write-back primitives for syncing local requirements files
5
+ * to the cloud, consumed by the comparator-driven sync path.
6
6
  */
7
- export { type CloudDocumentMetadata, type ConflictInfo, type DryRunResult, type DryRunResultItem, dryRunPush, executePush, extractMarkdownContent, type FileWithDryRun, type ParsedFile, type ParseFailure, type PushCredentials, type PushResult, parseFilesForPush, parseFilesForPushIndividually, WEB_APP_URL, } from "./core.js";
7
+ export { assertNoDuplicateDocumentIds, extractMarkdownContent, mergeMetadataWithRawFrontmatter, type ParsedFile, type ParseFailure, parseFilesForPush, parseFilesForPushIndividually, WEB_APP_URL, } from "./core.js";
8
8
  //# sourceMappingURL=index.d.ts.map
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Push Module
3
3
  *
4
- * Shared push logic for syncing local requirements files to the cloud.
5
- * Used by both CLI push command and MCP push_requirements tool.
4
+ * Shared parse-and-write-back primitives for syncing local requirements files
5
+ * to the cloud, consumed by the comparator-driven sync path.
6
6
  */
7
- export { dryRunPush, executePush,
7
+ export { assertNoDuplicateDocumentIds,
8
8
  // Functions
9
- extractMarkdownContent, parseFilesForPush, parseFilesForPushIndividually, WEB_APP_URL, } from "./core.js";
9
+ extractMarkdownContent, mergeMetadataWithRawFrontmatter, parseFilesForPush, parseFilesForPushIndividually, WEB_APP_URL, } from "./core.js";
10
10
  //# sourceMappingURL=index.js.map
@@ -312,9 +312,9 @@ ${body}
312
312
  ## Next Steps
313
313
 
314
314
  1. **Create file**: Save this template as \`${filePath}\` and edit it for your feature
315
- 2. **Refine style** (optional): Run \`style-check\` (or call the \`style_check\` MCP tool) for AI feedback
316
- 3. **Validate syntax**: Run \`validate\` (or call the \`validate\` MCP tool) to verify format
317
- 4. **Push to cloud**: Run \`dotrequirements push\` (or call the \`push_requirements\` MCP tool) to sync
315
+ 2. **Refine style** (optional): Run \`style-check\` for AI feedback
316
+ 3. **Validate syntax**: Run \`validate\` to verify format
317
+ 4. **Sync to cloud**: Run \`dotrequirements sync\` to reconcile the repo and the cloud
318
318
 
319
319
  **Note**: Requirements files can be colocated with code (\`src/auth.requirements.md\`) or centralized in \`.requirements/\` directory.`;
320
320
  }
@@ -140,7 +140,7 @@ export function validateForPush(metadata) {
140
140
  if (!metadata.document) {
141
141
  return {
142
142
  valid: false,
143
- reason: "Missing document section. Add a document section with a title to push this file.",
143
+ reason: "Missing document section. Add a document section with a title to sync this file.",
144
144
  };
145
145
  }
146
146
  // Must have title
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The repo/cloud comparator (DIFF-*). A pure function of two snapshots: it
3
+ * reads no disk and makes no network calls. `dotreq diff` renders its result,
4
+ * `dotreq sync` acts on it, CI asserts on it.
5
+ */
6
+ import type { CloudSnapshot, ComparisonResult, LocalSnapshot } from "./types.js";
7
+ /**
8
+ * Two local files carrying the same document id would both pair to one cloud
9
+ * document and both try to write it — last writer wins, silently destroying the
10
+ * first (SYNC-FAIL-3). Return the offending ids with their file paths so the
11
+ * caller can abort before any write.
12
+ */
13
+ export declare function duplicateLocalDocumentIds(local: LocalSnapshot): Array<{
14
+ documentId: string;
15
+ filePaths: string[];
16
+ }>;
17
+ /**
18
+ * Compare the whole repo against the whole cloud, pairing documents by id.
19
+ */
20
+ export declare function compareSnapshots(local: LocalSnapshot, cloud: CloudSnapshot): ComparisonResult;
21
+ //# sourceMappingURL=compare.d.ts.map