@popoverai/dotrequirements 0.28.0 → 0.29.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/codebase-to-spec/compose.d.ts +3 -2
- package/dist/codebase-to-spec/compose.js +3 -3
- package/dist/commands/sync.js +5 -0
- package/dist/push/core.js +7 -0
- package/dist/requirements/index.d.ts +2 -0
- package/dist/requirements/index.js +6 -1
- package/dist/requirements/style-guide.js +1 -1
- package/dist/schema/browser.d.ts +1 -0
- package/dist/schema/browser.js +3 -0
- package/dist/schema/index.d.ts +1 -0
- package/dist/schema/index.js +1 -0
- package/dist/schema/parser.d.ts +3 -0
- package/dist/schema/parser.js +1 -1
- package/dist/schema/schemas.d.ts +24 -22
- package/dist/schema/schemas.js +9 -12
- package/dist/schema/title-markdown.d.ts +30 -0
- package/dist/schema/title-markdown.js +38 -0
- package/dist/sync/execute.d.ts +5 -0
- package/dist/sync/execute.js +44 -5
- package/dist/sync/local-files.js +3 -1
- package/dist/sync/snapshot.js +6 -2
- package/dist/templates/example-requirements.js +0 -2
- package/dist/templates/example-requirements.ts +0 -2
- package/package.json +1 -1
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Compose stage: assemble per-area partials into a single composed spec.
|
|
3
3
|
*
|
|
4
|
-
* The composed document has YAML frontmatter (
|
|
5
|
-
*
|
|
4
|
+
* The composed document has YAML frontmatter (defaultPrefix), an H1 title —
|
|
5
|
+
* the document's name (SYNC-TITLE-1) — a summary paragraph from the outline,
|
|
6
|
+
* and one H2 section per area.
|
|
6
7
|
* Each area section contains the partial's content.
|
|
7
8
|
*
|
|
8
9
|
* After assembly, a deterministic renumbering pass rewrites each prefix's
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Compose stage: assemble per-area partials into a single composed spec.
|
|
3
3
|
*
|
|
4
|
-
* The composed document has YAML frontmatter (
|
|
5
|
-
*
|
|
4
|
+
* The composed document has YAML frontmatter (defaultPrefix), an H1 title —
|
|
5
|
+
* the document's name (SYNC-TITLE-1) — a summary paragraph from the outline,
|
|
6
|
+
* and one H2 section per area.
|
|
6
7
|
* Each area section contains the partial's content.
|
|
7
8
|
*
|
|
8
9
|
* After assembly, a deterministic renumbering pass rewrites each prefix's
|
|
@@ -48,7 +49,6 @@ export function assembleComposedDocument(outline, partialPathFor) {
|
|
|
48
49
|
const parts = [];
|
|
49
50
|
parts.push("---");
|
|
50
51
|
parts.push("document:");
|
|
51
|
-
parts.push(` title: "${escapeYamlString(outline.title)}"`);
|
|
52
52
|
parts.push(` defaultPrefix: ${outline.defaultPrefix}`);
|
|
53
53
|
parts.push("---");
|
|
54
54
|
parts.push("");
|
package/dist/commands/sync.js
CHANGED
|
@@ -114,6 +114,11 @@ export async function syncCommand(scope, options) {
|
|
|
114
114
|
for (const err of outcome.errors) {
|
|
115
115
|
console.log(` ✗ failed: ${err.name} — ${err.error}`);
|
|
116
116
|
}
|
|
117
|
+
// SYNC-TITLE-1.3: a push that changed a title is a rename — say so, so an
|
|
118
|
+
// unintended H1 edit is visible and costs one heading edit to undo.
|
|
119
|
+
for (const r of outcome.renames) {
|
|
120
|
+
console.log(` renamed "${r.from}" → "${r.to}"`);
|
|
121
|
+
}
|
|
117
122
|
// IMPORT-3: marker failures are loud but never fatal.
|
|
118
123
|
for (const { name, status } of outcome.importWarnings) {
|
|
119
124
|
if (status === "already_used") {
|
package/dist/push/core.js
CHANGED
|
@@ -65,6 +65,13 @@ export function mergeMetadataWithRawFrontmatter(metadata, rawFrontmatter) {
|
|
|
65
65
|
...rawDocument,
|
|
66
66
|
...metadata.document,
|
|
67
67
|
};
|
|
68
|
+
// SYNC-TITLE-1.2: `document.title` is a recognized-but-retired field —
|
|
69
|
+
// the title lives in the body as its leading H1 — so a rewrite drops it
|
|
70
|
+
// rather than preserving it as if it were a user's custom field
|
|
71
|
+
// (DOC-HEADER-14 protects unrecognized fields, not this one).
|
|
72
|
+
if (!("title" in metadata.document)) {
|
|
73
|
+
delete merged.document.title;
|
|
74
|
+
}
|
|
68
75
|
}
|
|
69
76
|
return merged;
|
|
70
77
|
}
|
|
@@ -17,6 +17,8 @@ export interface FlattenedRequirement {
|
|
|
17
17
|
export interface ParsedRequirementsFile {
|
|
18
18
|
metadata: Metadata;
|
|
19
19
|
requirements: RequirementNode[];
|
|
20
|
+
/** Markdown body; its leading H1 is the document's title (SYNC-TITLE-1). */
|
|
21
|
+
body?: string;
|
|
20
22
|
}
|
|
21
23
|
/**
|
|
22
24
|
* Find all *.requirements.md files in the workspace.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
2
|
import { glob, globSync } from "glob";
|
|
3
3
|
import { buildRequirementMarkdown, getAllRequirements, parseRequirementsFile, parseRequirementsFromFile, } from "../schema/index.js";
|
|
4
|
+
import { splitLeadingH1 } from "../schema/title-markdown.js";
|
|
4
5
|
/**
|
|
5
6
|
* Glob pattern matching *.requirements.md files.
|
|
6
7
|
*/
|
|
@@ -90,7 +91,11 @@ export function flattenRequirementsFile(file, sourceFile) {
|
|
|
90
91
|
content: req.content,
|
|
91
92
|
path: idParts.slice(1), // ["0", "1"] for "REQ123.0.1"
|
|
92
93
|
sourceFile: sourceFile,
|
|
93
|
-
|
|
94
|
+
// SYNC-TITLE-1: the body's leading H1 is the title; legacy
|
|
95
|
+
// frontmatter title as fallback for pre-H1 files
|
|
96
|
+
documentTitle: splitLeadingH1(file.body ?? "").title ||
|
|
97
|
+
file.metadata.document?.title ||
|
|
98
|
+
rootId,
|
|
94
99
|
});
|
|
95
100
|
}
|
|
96
101
|
return results;
|
package/dist/schema/browser.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export { buildMetadata, constructKey, convexToRequirements, extractRequirementKe
|
|
|
9
9
|
export { DELIMITER_PATTERN, extractRequirementBlocks, findRequirementById, flattenRequirementTree, getAllRequirements, parseCriterionLine, parseRequirementBlock, parseRequirementBlocksFromMarkdown, parseRootLine, ROOT_LABEL_MARKER, } from "./parser-core.js";
|
|
10
10
|
export type { Metadata, ParsedCriterion, RequirementKey, RequirementNode, RequirementPrefix, RequirementsFile, } from "./schemas.js";
|
|
11
11
|
export { buildRequirementKey, MetadataSchema, normalizePrefix, ParsedCriterionSchema, parseRequirementKey, REQUIREMENT_KEY_PATTERN, REQUIREMENT_PREFIX_PATTERN, RequirementKeySchema, RequirementNodeSchema, RequirementPrefixSchema, RequirementsFileSchema, ValidationError, validateKey, validateMetadata, validatePrefix, validateRequirementNode, validateRequirementsFile, } from "./schemas.js";
|
|
12
|
+
export { composeMarkdownWithTitle, type SplitMarkdown, splitLeadingH1, } from "./title-markdown.js";
|
|
12
13
|
export type { BuildScenarioOptions, Scenario, ScenarioAssertionSource, ScenarioStep, } from "./scenario.js";
|
|
13
14
|
export { buildScenarioFromRequirements, requirementTreeToScenario, } from "./scenario.js";
|
|
14
15
|
//# sourceMappingURL=browser.d.ts.map
|
package/dist/schema/browser.js
CHANGED
|
@@ -22,6 +22,9 @@ normalizePrefix, ParsedCriterionSchema, parseRequirementKey, REQUIREMENT_KEY_PAT
|
|
|
22
22
|
REQUIREMENT_PREFIX_PATTERN, RequirementKeySchema, RequirementNodeSchema, RequirementPrefixSchema, RequirementsFileSchema,
|
|
23
23
|
// Validation
|
|
24
24
|
ValidationError, validateKey, validateMetadata, validatePrefix, validateRequirementNode, validateRequirementsFile, } from "./schemas.js";
|
|
25
|
+
// Title semantics (pure TypeScript - browser-safe): the document title and
|
|
26
|
+
// the markdown's leading H1 are the same thing (DOC-TITLE-3)
|
|
27
|
+
export { composeMarkdownWithTitle, splitLeadingH1, } from "./title-markdown.js";
|
|
25
28
|
// Scenario building (pure TypeScript - browser-safe, used by Convex Node actions)
|
|
26
29
|
export { buildScenarioFromRequirements, requirementTreeToScenario, } from "./scenario.js";
|
|
27
30
|
//# sourceMappingURL=browser.js.map
|
package/dist/schema/index.d.ts
CHANGED
|
@@ -13,4 +13,5 @@ export type { BuildScenarioOptions, Scenario, ScenarioAssertionSource, ScenarioS
|
|
|
13
13
|
export { buildScenarioFromRequirements, requirementTreeToScenario, } from "./scenario.js";
|
|
14
14
|
export type { Metadata, ParsedCriterion, PushValidationResult, RequirementKey, RequirementNode, RequirementPrefix, RequirementsFile, } from "./schemas.js";
|
|
15
15
|
export { buildRequirementKey, CONVEX_ID_PATTERN, MetadataSchema, normalizePrefix, ParsedCriterionSchema, parseRequirementKey, REQUIREMENT_KEY_PATTERN, REQUIREMENT_PREFIX_PATTERN, RequirementKeySchema, RequirementNodeSchema, RequirementPrefixSchema, RequirementsFileSchema, ValidationError, validateForPush, validateKey, validateMetadata, validatePrefix, validateRequirementNode, validateRequirementsFile, } from "./schemas.js";
|
|
16
|
+
export { composeMarkdownWithTitle, type SplitMarkdown, splitLeadingH1, } from "./title-markdown.js";
|
|
16
17
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/schema/index.js
CHANGED
|
@@ -23,4 +23,5 @@ normalizePrefix, ParsedCriterionSchema, parseRequirementKey, REQUIREMENT_KEY_PAT
|
|
|
23
23
|
REQUIREMENT_PREFIX_PATTERN, RequirementKeySchema, RequirementNodeSchema, RequirementPrefixSchema, RequirementsFileSchema,
|
|
24
24
|
// Validation
|
|
25
25
|
ValidationError, validateForPush, validateKey, validateMetadata, validatePrefix, validateRequirementNode, validateRequirementsFile, } from "./schemas.js";
|
|
26
|
+
export { composeMarkdownWithTitle, splitLeadingH1, } from "./title-markdown.js";
|
|
26
27
|
//# sourceMappingURL=index.js.map
|
package/dist/schema/parser.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ export { findRequirementById, flattenRequirementTree, getAllRequirements, parseC
|
|
|
15
15
|
export declare function parseRequirementsFile(markdownContent: string): {
|
|
16
16
|
metadata: RequirementsFile["_meta"];
|
|
17
17
|
requirements: RequirementNode[];
|
|
18
|
+
/** The markdown body (frontmatter stripped) — its leading H1 is the document's title (SYNC-TITLE-1). */
|
|
19
|
+
body: string;
|
|
18
20
|
};
|
|
19
21
|
/**
|
|
20
22
|
* Parse requirements file from disk.
|
|
@@ -22,5 +24,6 @@ export declare function parseRequirementsFile(markdownContent: string): {
|
|
|
22
24
|
export declare function parseRequirementsFromFile(filePath: string): {
|
|
23
25
|
metadata: RequirementsFile["_meta"];
|
|
24
26
|
requirements: RequirementNode[];
|
|
27
|
+
body: string;
|
|
25
28
|
};
|
|
26
29
|
//# sourceMappingURL=parser.d.ts.map
|
package/dist/schema/parser.js
CHANGED
|
@@ -47,7 +47,7 @@ export function parseRequirementsFile(markdownContent) {
|
|
|
47
47
|
if (requirements.length === 0) {
|
|
48
48
|
throw new ValidationError("No requirement blocks found in document");
|
|
49
49
|
}
|
|
50
|
-
return { metadata, requirements };
|
|
50
|
+
return { metadata, requirements, body };
|
|
51
51
|
}
|
|
52
52
|
/**
|
|
53
53
|
* Parse requirements file from disk.
|
package/dist/schema/schemas.d.ts
CHANGED
|
@@ -54,8 +54,10 @@ export declare function parseRequirementKey(key: string): {
|
|
|
54
54
|
*/
|
|
55
55
|
export declare function buildRequirementKey(prefix: string, number: number): string;
|
|
56
56
|
/**
|
|
57
|
-
* Metadata block at the top of every requirements file.
|
|
58
|
-
*
|
|
57
|
+
* Metadata block at the top of every requirements file. Everything is
|
|
58
|
+
* optional or auto-filled; the document's title lives in the BODY as its
|
|
59
|
+
* leading H1 (SYNC-TITLE-1), with a legacy frontmatter title tolerated as a
|
|
60
|
+
* fallback for files from before that convention.
|
|
59
61
|
*/
|
|
60
62
|
export declare const MetadataSchema: z.ZodObject<{
|
|
61
63
|
version: z.ZodOptional<z.ZodNumber>;
|
|
@@ -63,15 +65,15 @@ export declare const MetadataSchema: z.ZodObject<{
|
|
|
63
65
|
ctsRun: z.ZodOptional<z.ZodString>;
|
|
64
66
|
document: z.ZodOptional<z.ZodObject<{
|
|
65
67
|
id: z.ZodOptional<z.ZodString>;
|
|
66
|
-
title: z.ZodString
|
|
68
|
+
title: z.ZodOptional<z.ZodString>;
|
|
67
69
|
defaultPrefix: z.ZodOptional<z.ZodString>;
|
|
68
70
|
}, "strip", z.ZodTypeAny, {
|
|
69
|
-
title: string;
|
|
70
71
|
id?: string | undefined;
|
|
72
|
+
title?: string | undefined;
|
|
71
73
|
defaultPrefix?: string | undefined;
|
|
72
74
|
}, {
|
|
73
|
-
title: string;
|
|
74
75
|
id?: string | undefined;
|
|
76
|
+
title?: string | undefined;
|
|
75
77
|
defaultPrefix?: string | undefined;
|
|
76
78
|
}>>;
|
|
77
79
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -79,8 +81,8 @@ export declare const MetadataSchema: z.ZodObject<{
|
|
|
79
81
|
pulledAt?: string | undefined;
|
|
80
82
|
ctsRun?: string | undefined;
|
|
81
83
|
document?: {
|
|
82
|
-
title: string;
|
|
83
84
|
id?: string | undefined;
|
|
85
|
+
title?: string | undefined;
|
|
84
86
|
defaultPrefix?: string | undefined;
|
|
85
87
|
} | undefined;
|
|
86
88
|
}, {
|
|
@@ -88,8 +90,8 @@ export declare const MetadataSchema: z.ZodObject<{
|
|
|
88
90
|
pulledAt?: string | undefined;
|
|
89
91
|
ctsRun?: string | undefined;
|
|
90
92
|
document?: {
|
|
91
|
-
title: string;
|
|
92
93
|
id?: string | undefined;
|
|
94
|
+
title?: string | undefined;
|
|
93
95
|
defaultPrefix?: string | undefined;
|
|
94
96
|
} | undefined;
|
|
95
97
|
}>;
|
|
@@ -152,15 +154,15 @@ export declare const RequirementsFileSchema: z.ZodObject<{
|
|
|
152
154
|
ctsRun: z.ZodOptional<z.ZodString>;
|
|
153
155
|
document: z.ZodOptional<z.ZodObject<{
|
|
154
156
|
id: z.ZodOptional<z.ZodString>;
|
|
155
|
-
title: z.ZodString
|
|
157
|
+
title: z.ZodOptional<z.ZodString>;
|
|
156
158
|
defaultPrefix: z.ZodOptional<z.ZodString>;
|
|
157
159
|
}, "strip", z.ZodTypeAny, {
|
|
158
|
-
title: string;
|
|
159
160
|
id?: string | undefined;
|
|
161
|
+
title?: string | undefined;
|
|
160
162
|
defaultPrefix?: string | undefined;
|
|
161
163
|
}, {
|
|
162
|
-
title: string;
|
|
163
164
|
id?: string | undefined;
|
|
165
|
+
title?: string | undefined;
|
|
164
166
|
defaultPrefix?: string | undefined;
|
|
165
167
|
}>>;
|
|
166
168
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -168,8 +170,8 @@ export declare const RequirementsFileSchema: z.ZodObject<{
|
|
|
168
170
|
pulledAt?: string | undefined;
|
|
169
171
|
ctsRun?: string | undefined;
|
|
170
172
|
document?: {
|
|
171
|
-
title: string;
|
|
172
173
|
id?: string | undefined;
|
|
174
|
+
title?: string | undefined;
|
|
173
175
|
defaultPrefix?: string | undefined;
|
|
174
176
|
} | undefined;
|
|
175
177
|
}, {
|
|
@@ -177,8 +179,8 @@ export declare const RequirementsFileSchema: z.ZodObject<{
|
|
|
177
179
|
pulledAt?: string | undefined;
|
|
178
180
|
ctsRun?: string | undefined;
|
|
179
181
|
document?: {
|
|
180
|
-
title: string;
|
|
181
182
|
id?: string | undefined;
|
|
183
|
+
title?: string | undefined;
|
|
182
184
|
defaultPrefix?: string | undefined;
|
|
183
185
|
} | undefined;
|
|
184
186
|
}>;
|
|
@@ -189,15 +191,15 @@ export declare const RequirementsFileSchema: z.ZodObject<{
|
|
|
189
191
|
ctsRun: z.ZodOptional<z.ZodString>;
|
|
190
192
|
document: z.ZodOptional<z.ZodObject<{
|
|
191
193
|
id: z.ZodOptional<z.ZodString>;
|
|
192
|
-
title: z.ZodString
|
|
194
|
+
title: z.ZodOptional<z.ZodString>;
|
|
193
195
|
defaultPrefix: z.ZodOptional<z.ZodString>;
|
|
194
196
|
}, "strip", z.ZodTypeAny, {
|
|
195
|
-
title: string;
|
|
196
197
|
id?: string | undefined;
|
|
198
|
+
title?: string | undefined;
|
|
197
199
|
defaultPrefix?: string | undefined;
|
|
198
200
|
}, {
|
|
199
|
-
title: string;
|
|
200
201
|
id?: string | undefined;
|
|
202
|
+
title?: string | undefined;
|
|
201
203
|
defaultPrefix?: string | undefined;
|
|
202
204
|
}>>;
|
|
203
205
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -205,8 +207,8 @@ export declare const RequirementsFileSchema: z.ZodObject<{
|
|
|
205
207
|
pulledAt?: string | undefined;
|
|
206
208
|
ctsRun?: string | undefined;
|
|
207
209
|
document?: {
|
|
208
|
-
title: string;
|
|
209
210
|
id?: string | undefined;
|
|
211
|
+
title?: string | undefined;
|
|
210
212
|
defaultPrefix?: string | undefined;
|
|
211
213
|
} | undefined;
|
|
212
214
|
}, {
|
|
@@ -214,8 +216,8 @@ export declare const RequirementsFileSchema: z.ZodObject<{
|
|
|
214
216
|
pulledAt?: string | undefined;
|
|
215
217
|
ctsRun?: string | undefined;
|
|
216
218
|
document?: {
|
|
217
|
-
title: string;
|
|
218
219
|
id?: string | undefined;
|
|
220
|
+
title?: string | undefined;
|
|
219
221
|
defaultPrefix?: string | undefined;
|
|
220
222
|
} | undefined;
|
|
221
223
|
}>;
|
|
@@ -226,15 +228,15 @@ export declare const RequirementsFileSchema: z.ZodObject<{
|
|
|
226
228
|
ctsRun: z.ZodOptional<z.ZodString>;
|
|
227
229
|
document: z.ZodOptional<z.ZodObject<{
|
|
228
230
|
id: z.ZodOptional<z.ZodString>;
|
|
229
|
-
title: z.ZodString
|
|
231
|
+
title: z.ZodOptional<z.ZodString>;
|
|
230
232
|
defaultPrefix: z.ZodOptional<z.ZodString>;
|
|
231
233
|
}, "strip", z.ZodTypeAny, {
|
|
232
|
-
title: string;
|
|
233
234
|
id?: string | undefined;
|
|
235
|
+
title?: string | undefined;
|
|
234
236
|
defaultPrefix?: string | undefined;
|
|
235
237
|
}, {
|
|
236
|
-
title: string;
|
|
237
238
|
id?: string | undefined;
|
|
239
|
+
title?: string | undefined;
|
|
238
240
|
defaultPrefix?: string | undefined;
|
|
239
241
|
}>>;
|
|
240
242
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -242,8 +244,8 @@ export declare const RequirementsFileSchema: z.ZodObject<{
|
|
|
242
244
|
pulledAt?: string | undefined;
|
|
243
245
|
ctsRun?: string | undefined;
|
|
244
246
|
document?: {
|
|
245
|
-
title: string;
|
|
246
247
|
id?: string | undefined;
|
|
248
|
+
title?: string | undefined;
|
|
247
249
|
defaultPrefix?: string | undefined;
|
|
248
250
|
} | undefined;
|
|
249
251
|
}, {
|
|
@@ -251,8 +253,8 @@ export declare const RequirementsFileSchema: z.ZodObject<{
|
|
|
251
253
|
pulledAt?: string | undefined;
|
|
252
254
|
ctsRun?: string | undefined;
|
|
253
255
|
document?: {
|
|
254
|
-
title: string;
|
|
255
256
|
id?: string | undefined;
|
|
257
|
+
title?: string | undefined;
|
|
256
258
|
defaultPrefix?: string | undefined;
|
|
257
259
|
} | undefined;
|
|
258
260
|
}>;
|
package/dist/schema/schemas.js
CHANGED
|
@@ -106,8 +106,10 @@ export function buildRequirementKey(prefix, number) {
|
|
|
106
106
|
// Metadata Schema
|
|
107
107
|
// =============================================================================
|
|
108
108
|
/**
|
|
109
|
-
* Metadata block at the top of every requirements file.
|
|
110
|
-
*
|
|
109
|
+
* Metadata block at the top of every requirements file. Everything is
|
|
110
|
+
* optional or auto-filled; the document's title lives in the BODY as its
|
|
111
|
+
* leading H1 (SYNC-TITLE-1), with a legacy frontmatter title tolerated as a
|
|
112
|
+
* fallback for files from before that convention.
|
|
111
113
|
*/
|
|
112
114
|
export const MetadataSchema = z.object({
|
|
113
115
|
version: z.number().optional(), // Informational - incremented on push
|
|
@@ -116,7 +118,7 @@ export const MetadataSchema = z.object({
|
|
|
116
118
|
document: z
|
|
117
119
|
.object({
|
|
118
120
|
id: z.string().optional(), // Cloud document link - filled by push when creating
|
|
119
|
-
title: z.string(), //
|
|
121
|
+
title: z.string().optional(), // Legacy fallback — the body's leading H1 is the title
|
|
120
122
|
defaultPrefix: z.string().optional(), // Editor hint for new requirement keys
|
|
121
123
|
})
|
|
122
124
|
.optional(),
|
|
@@ -136,18 +138,13 @@ export const CONVEX_ID_PATTERN = /^j[a-z0-9]{31}$/;
|
|
|
136
138
|
* SYNC-CLI-EDIT-1: Files with valid document.id update existing documents
|
|
137
139
|
*/
|
|
138
140
|
export function validateForPush(metadata) {
|
|
139
|
-
// Must have document section
|
|
141
|
+
// Must have document section. (A title is NOT required — untitled
|
|
142
|
+
// documents are allowed, and the title is derived from the markdown's
|
|
143
|
+
// leading H1 anyway; see docs/working/h1-as-title.md.)
|
|
140
144
|
if (!metadata.document) {
|
|
141
145
|
return {
|
|
142
146
|
valid: false,
|
|
143
|
-
reason: "Missing document section. Add a document section
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
// Must have title
|
|
147
|
-
if (!metadata.document.title || metadata.document.title.trim() === "") {
|
|
148
|
-
return {
|
|
149
|
-
valid: false,
|
|
150
|
-
reason: "Document section missing required title.",
|
|
147
|
+
reason: "Missing document section. Add a document section to sync this file.",
|
|
151
148
|
};
|
|
152
149
|
}
|
|
153
150
|
// Check document.id
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The document title and the markdown's leading H1 are the same thing
|
|
3
|
+
* (DOC-TITLE-3). These helpers hold that contract at the web app's storage
|
|
4
|
+
* boundary: markdown coming *in* from Convex is split into title + body, and
|
|
5
|
+
* markdown going *out* is recomposed — so inside the app, `Content.markdown`
|
|
6
|
+
* is always the body (no leading H1) and `Content.title` carries the title.
|
|
7
|
+
*/
|
|
8
|
+
export interface SplitMarkdown {
|
|
9
|
+
/** The leading H1's text, or null when the markdown has no leading H1. */
|
|
10
|
+
title: string | null;
|
|
11
|
+
/** Everything after the leading H1 (or the whole input when there is none). */
|
|
12
|
+
body: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Split a leading ATX H1 (`# Title`) off the markdown.
|
|
16
|
+
*
|
|
17
|
+
* "Leading" means the first non-blank line (DOC-TITLE-3.0); an H1 further
|
|
18
|
+
* down is ordinary content and stays in the body (DOC-TITLE-3.4). Markdown
|
|
19
|
+
* allows up to 3 spaces of heading indentation — 4+ is a code block, so it
|
|
20
|
+
* is not a title.
|
|
21
|
+
*/
|
|
22
|
+
export declare function splitLeadingH1(markdown: string): SplitMarkdown;
|
|
23
|
+
/**
|
|
24
|
+
* Compose full-document markdown from a title and a body.
|
|
25
|
+
*
|
|
26
|
+
* The title becomes the leading H1 (DOC-TITLE-3.1); an untitled document
|
|
27
|
+
* gets no empty heading line (DOC-TITLE-3.3).
|
|
28
|
+
*/
|
|
29
|
+
export declare function composeMarkdownWithTitle(title: string, body: string): string;
|
|
30
|
+
//# sourceMappingURL=title-markdown.d.ts.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The document title and the markdown's leading H1 are the same thing
|
|
3
|
+
* (DOC-TITLE-3). These helpers hold that contract at the web app's storage
|
|
4
|
+
* boundary: markdown coming *in* from Convex is split into title + body, and
|
|
5
|
+
* markdown going *out* is recomposed — so inside the app, `Content.markdown`
|
|
6
|
+
* is always the body (no leading H1) and `Content.title` carries the title.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Split a leading ATX H1 (`# Title`) off the markdown.
|
|
10
|
+
*
|
|
11
|
+
* "Leading" means the first non-blank line (DOC-TITLE-3.0); an H1 further
|
|
12
|
+
* down is ordinary content and stays in the body (DOC-TITLE-3.4). Markdown
|
|
13
|
+
* allows up to 3 spaces of heading indentation — 4+ is a code block, so it
|
|
14
|
+
* is not a title.
|
|
15
|
+
*/
|
|
16
|
+
export function splitLeadingH1(markdown) {
|
|
17
|
+
const match = /^(?:[ \t]*\r?\n)* {0,3}# (.*)(?:\r?\n|$)/.exec(markdown);
|
|
18
|
+
if (!match) {
|
|
19
|
+
return { title: null, body: markdown };
|
|
20
|
+
}
|
|
21
|
+
const body = markdown.slice(match[0].length).replace(/^(\s*\r?\n)+/, "");
|
|
22
|
+
return { title: match[1].trim(), body };
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Compose full-document markdown from a title and a body.
|
|
26
|
+
*
|
|
27
|
+
* The title becomes the leading H1 (DOC-TITLE-3.1); an untitled document
|
|
28
|
+
* gets no empty heading line (DOC-TITLE-3.3).
|
|
29
|
+
*/
|
|
30
|
+
export function composeMarkdownWithTitle(title, body) {
|
|
31
|
+
const trimmedTitle = title.trim();
|
|
32
|
+
if (!trimmedTitle)
|
|
33
|
+
return body;
|
|
34
|
+
if (!body.trim())
|
|
35
|
+
return `# ${trimmedTitle}\n`;
|
|
36
|
+
return `# ${trimmedTitle}\n\n${body}`;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=title-markdown.js.map
|
package/dist/sync/execute.d.ts
CHANGED
|
@@ -38,6 +38,11 @@ export interface SyncOutcome {
|
|
|
38
38
|
name: string;
|
|
39
39
|
status: string;
|
|
40
40
|
}>;
|
|
41
|
+
/** SYNC-TITLE-1.3: pushes that changed a document's title. */
|
|
42
|
+
renames: Array<{
|
|
43
|
+
from: string;
|
|
44
|
+
to: string;
|
|
45
|
+
}>;
|
|
41
46
|
}
|
|
42
47
|
/**
|
|
43
48
|
* Execute a plan. `cloud` provides bodies for local writes (keyed by id). Cloud
|
package/dist/sync/execute.js
CHANGED
|
@@ -11,7 +11,7 @@ import { ConvexHttpClient } from "convex/browser";
|
|
|
11
11
|
import { getConvexUrl } from "../config.js";
|
|
12
12
|
import { api } from "../convex.js";
|
|
13
13
|
import { mergeMetadataWithRawFrontmatter, parseFilesForPushIndividually, WEB_APP_URL, } from "../push/core.js";
|
|
14
|
-
import { buildRequirementsFile } from "../schema/index.js";
|
|
14
|
+
import { buildRequirementsFile, composeMarkdownWithTitle, splitLeadingH1, } from "../schema/index.js";
|
|
15
15
|
import { resolveLocalPath, writeLocalDocument } from "./local-files.js";
|
|
16
16
|
function emptyOutcome() {
|
|
17
17
|
return {
|
|
@@ -26,6 +26,7 @@ function emptyOutcome() {
|
|
|
26
26
|
synced: [],
|
|
27
27
|
writeBackWarnings: [],
|
|
28
28
|
importWarnings: [],
|
|
29
|
+
renames: [],
|
|
29
30
|
};
|
|
30
31
|
}
|
|
31
32
|
/**
|
|
@@ -52,7 +53,7 @@ export async function executePlan(plan, cloud, auth, workspaceRoot) {
|
|
|
52
53
|
outcome.invalid.push(doc);
|
|
53
54
|
break;
|
|
54
55
|
case "cloud_write":
|
|
55
|
-
await cloudWrite(client, auth, doc, outcome);
|
|
56
|
+
await cloudWrite(client, auth, doc, cloudById, outcome);
|
|
56
57
|
break;
|
|
57
58
|
case "cloud_delete":
|
|
58
59
|
await cloudDelete(client, auth, doc, outcome);
|
|
@@ -82,7 +83,7 @@ function errorDisplayMessage(err) {
|
|
|
82
83
|
return data.message;
|
|
83
84
|
return err instanceof Error ? err.message : String(err);
|
|
84
85
|
}
|
|
85
|
-
async function cloudWrite(client, auth, doc, outcome) {
|
|
86
|
+
async function cloudWrite(client, auth, doc, cloudById, outcome) {
|
|
86
87
|
if (!auth.projectSlug || !doc.filePath) {
|
|
87
88
|
throw new Error("Cannot write to the cloud without project credentials.");
|
|
88
89
|
}
|
|
@@ -91,6 +92,21 @@ async function cloudWrite(client, auth, doc, outcome) {
|
|
|
91
92
|
if (!parsed)
|
|
92
93
|
throw new Error(`Could not parse ${path.basename(doc.filePath)}`);
|
|
93
94
|
const meta = parsed.metadata.document;
|
|
95
|
+
// SYNC-TITLE-1.1: a legacy file whose title lives only in frontmatter (no body
|
|
96
|
+
// H1) must reach the cloud self-describing. Materialize the title as the body's
|
|
97
|
+
// leading H1 BEFORE the push — so the cloud row carries the H1 on first write
|
|
98
|
+
// and never lands as an H1-less-but-titled row. Such a row would otherwise be
|
|
99
|
+
// reachable to a web/MCP edit that untitles it (and is out of reach of the
|
|
100
|
+
// one-time title backfill, since a legacy checkout mints it on any later sync).
|
|
101
|
+
// Only the unambiguous case fires: a known frontmatter title with no existing
|
|
102
|
+
// H1 — never a guess about a content H1.
|
|
103
|
+
if (meta?.title && splitLeadingH1(parsed.markdownContent).title === null) {
|
|
104
|
+
parsed.markdownContent = composeMarkdownWithTitle(meta.title, parsed.markdownContent);
|
|
105
|
+
}
|
|
106
|
+
// SYNC-TITLE-1: the server derives the title from the body's leading H1.
|
|
107
|
+
// A legacy frontmatter title rides along only as the server's fallback for
|
|
108
|
+
// H1-less bodies (SYNC-TITLE-1.1); the filename is never a title
|
|
109
|
+
// (SYNC-TITLE-1.0).
|
|
94
110
|
const saveResult = (await client.mutation(api.documents.saveWithRequirements.saveWithRequirements, {
|
|
95
111
|
projectAuth: {
|
|
96
112
|
projectSlug: auth.projectSlug,
|
|
@@ -98,7 +114,7 @@ async function cloudWrite(client, auth, doc, outcome) {
|
|
|
98
114
|
},
|
|
99
115
|
target: { type: "project", slug: auth.projectSlug },
|
|
100
116
|
documentId: meta?.id,
|
|
101
|
-
title: meta?.title
|
|
117
|
+
title: meta?.title,
|
|
102
118
|
markdownContent: parsed.markdownContent,
|
|
103
119
|
defaultPrefix: meta?.defaultPrefix,
|
|
104
120
|
runMarker: parsed.metadata.ctsRun,
|
|
@@ -125,6 +141,24 @@ async function cloudWrite(client, auth, doc, outcome) {
|
|
|
125
141
|
outcome.cloudCreated++;
|
|
126
142
|
else
|
|
127
143
|
outcome.cloudUpdated++;
|
|
144
|
+
// SYNC-TITLE-1.3: a push that changes the title is a rename — announce it,
|
|
145
|
+
// so an unintended H1 edit is visible and costs one heading edit to undo.
|
|
146
|
+
// The new title is derived from the pushed markdown exactly as the server
|
|
147
|
+
// derives it (H1, else the legacy frontmatter title, else untitled) — NOT
|
|
148
|
+
// from doc.title, which the comparator pins to the OLD cloud title for any
|
|
149
|
+
// id-matched document, so comparing against it never detects a rename.
|
|
150
|
+
const cloudBefore = meta?.id ? cloudById.get(meta.id) : undefined;
|
|
151
|
+
if (cloudBefore) {
|
|
152
|
+
const newTitle = splitLeadingH1(parsed.markdownContent).title ?? meta?.title ?? "";
|
|
153
|
+
// NAV-TITLE-1.2: the cloud snapshot reports an untitled document's title as
|
|
154
|
+
// the display fallback "Untitled Document", while newTitle is "" for that
|
|
155
|
+
// same state. Normalize before comparing so a content-only push of an
|
|
156
|
+
// untitled doc doesn't report a phantom `"Untitled Document" → ""` rename.
|
|
157
|
+
const before = cloudBefore.title === "Untitled Document" ? "" : cloudBefore.title;
|
|
158
|
+
if (before !== newTitle) {
|
|
159
|
+
outcome.renames.push({ from: before, to: newTitle });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
128
162
|
outcome.synced.push({
|
|
129
163
|
name: path.basename(doc.filePath),
|
|
130
164
|
url: `${WEB_APP_URL}/documents/${documentId}`,
|
|
@@ -133,8 +167,13 @@ async function cloudWrite(client, auth, doc, outcome) {
|
|
|
133
167
|
// failure is a warning on a synced document, never a sync failure (which
|
|
134
168
|
// would invite a retry that mints a duplicate).
|
|
135
169
|
try {
|
|
136
|
-
if (parsed.metadata.document)
|
|
170
|
+
if (parsed.metadata.document) {
|
|
137
171
|
parsed.metadata.document.id = documentId;
|
|
172
|
+
// SYNC-TITLE-1.2: drop the legacy frontmatter title — the body already
|
|
173
|
+
// carries the name as its leading H1 (materialized above, before the
|
|
174
|
+
// push), so the rewritten file loses the field without losing the title.
|
|
175
|
+
delete parsed.metadata.document.title;
|
|
176
|
+
}
|
|
138
177
|
parsed.metadata.pulledAt = new Date().toISOString();
|
|
139
178
|
parsed.metadata.version = (parsed.metadata.version ?? 0) + 1;
|
|
140
179
|
const content = buildRequirementsFile(mergeMetadataWithRawFrontmatter(parsed.metadata, parsed.rawFrontmatter), parsed.markdownContent);
|
package/dist/sync/local-files.js
CHANGED
|
@@ -51,13 +51,15 @@ export function readCtsRun(filePath) {
|
|
|
51
51
|
*/
|
|
52
52
|
export function writeLocalDocument(filePath, cloud) {
|
|
53
53
|
const existingCtsRun = readCtsRun(filePath);
|
|
54
|
+
// SYNC-TITLE-1.2: no `document.title` in frontmatter — the title lives in
|
|
55
|
+
// the body as its leading H1. A legacy file that gets rewritten loses the
|
|
56
|
+
// field here, since the metadata is rebuilt from scratch.
|
|
54
57
|
const metadata = {
|
|
55
58
|
pulledAt: new Date().toISOString(),
|
|
56
59
|
version: cloud.version,
|
|
57
60
|
...(existingCtsRun ? { ctsRun: existingCtsRun } : {}),
|
|
58
61
|
document: {
|
|
59
62
|
id: cloud.documentId,
|
|
60
|
-
title: cloud.title,
|
|
61
63
|
defaultPrefix: cloud.defaultPrefix,
|
|
62
64
|
},
|
|
63
65
|
};
|
package/dist/sync/snapshot.js
CHANGED
|
@@ -9,7 +9,7 @@ import YAML from "yaml";
|
|
|
9
9
|
import { getConvexUrl } from "../config.js";
|
|
10
10
|
import { api } from "../convex.js";
|
|
11
11
|
import { findRequirementsFiles } from "../requirements/index.js";
|
|
12
|
-
import { validateMetadata } from "../schema/index.js";
|
|
12
|
+
import { splitLeadingH1, validateMetadata } from "../schema/index.js";
|
|
13
13
|
import { parseRequirementBlocksFromMarkdown, splitFrontmatter, stripFrontmatterBlock, } from "../schema/parser-core.js";
|
|
14
14
|
/**
|
|
15
15
|
* Read and classify one requirements file into a LocalDocument. A file whose
|
|
@@ -79,7 +79,11 @@ export function readLocalDocument(filePath) {
|
|
|
79
79
|
catch (err) {
|
|
80
80
|
return { filePath, documentId, title, parseError: err.message };
|
|
81
81
|
}
|
|
82
|
-
|
|
82
|
+
// SYNC-TITLE-1: the body's leading H1 is the document's title. A legacy
|
|
83
|
+
// frontmatter title stands only when the body has none (SYNC-TITLE-1.1);
|
|
84
|
+
// with neither, the document is untitled (SYNC-TITLE-1.4).
|
|
85
|
+
const derivedTitle = splitLeadingH1(body).title ?? title;
|
|
86
|
+
return { filePath, documentId, title: derivedTitle, defaultPrefix, body };
|
|
83
87
|
}
|
|
84
88
|
/**
|
|
85
89
|
* Build the local snapshot from the workspace, or from an explicit set of file
|