@popoverai/dotrequirements 0.29.0 → 0.29.2

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 (42) hide show
  1. package/dist/codebase-to-spec/compose.js +0 -4
  2. package/dist/codebase-to-spec/renumber.d.ts +7 -5
  3. package/dist/codebase-to-spec/renumber.js +17 -15
  4. package/dist/commands/report.js +15 -1
  5. package/dist/commands/sync.js +7 -1
  6. package/dist/harness/cache.d.ts +83 -2
  7. package/dist/harness/cache.js +94 -8
  8. package/dist/harness/finalize.js +238 -78
  9. package/dist/harness/reportingStatus.d.ts +44 -0
  10. package/dist/harness/reportingStatus.js +123 -0
  11. package/dist/push/core.d.ts +0 -12
  12. package/dist/push/core.js +8 -58
  13. package/dist/push/index.d.ts +1 -1
  14. package/dist/push/index.js +1 -1
  15. package/dist/requirements/cloud-coverage.js +7 -2
  16. package/dist/schema/browser.d.ts +2 -2
  17. package/dist/schema/browser.js +2 -2
  18. package/dist/schema/builder.d.ts +6 -1
  19. package/dist/schema/builder.js +6 -1
  20. package/dist/schema/conversions.js +12 -3
  21. package/dist/schema/file-writer.d.ts +49 -0
  22. package/dist/schema/file-writer.js +138 -0
  23. package/dist/schema/index.d.ts +5 -2
  24. package/dist/schema/index.js +3 -2
  25. package/dist/schema/parser-core.d.ts +32 -5
  26. package/dist/schema/parser-core.js +136 -31
  27. package/dist/schema/parser.d.ts +2 -1
  28. package/dist/schema/parser.js +1 -1
  29. package/dist/schema/schemas.d.ts +11 -0
  30. package/dist/schema/schemas.js +14 -0
  31. package/dist/sync/compare.js +16 -1
  32. package/dist/sync/execute.d.ts +10 -2
  33. package/dist/sync/execute.js +44 -18
  34. package/dist/sync/local-files.d.ts +2 -12
  35. package/dist/sync/local-files.js +2 -62
  36. package/dist/sync/segment.d.ts +2 -2
  37. package/dist/sync/segment.js +45 -11
  38. package/package.json +2 -2
  39. package/dist/harness/convexReporting.d.ts +0 -15
  40. package/dist/harness/convexReporting.js +0 -131
  41. package/dist/harness/coverageCache.d.ts +0 -30
  42. package/dist/harness/coverageCache.js +0 -70
package/dist/push/core.js CHANGED
@@ -5,9 +5,8 @@
5
5
  * `push` command (its behavior now lives in the sync executor).
6
6
  */
7
7
  import * as fs from "node:fs";
8
- import YAML from "yaml";
9
8
  import { getAllRequirements, parseRequirementKey, parseRequirementsFromFile, } from "../schema/index.js";
10
- import { extractFrontmatterBlock, stripFrontmatterBlock, } from "../schema/parser-core.js";
9
+ import { stripFrontmatterBlock } from "../schema/parser-core.js";
11
10
  /** Base URL of the web app, where synced documents are reviewed. */
12
11
  export const WEB_APP_URL = "https://app.dotrequirements.io";
13
12
  // ============================================================================
@@ -23,58 +22,6 @@ export function extractMarkdownContent(rawContent) {
23
22
  // body.
24
23
  return stripFrontmatterBlock(rawContent);
25
24
  }
26
- /**
27
- * DOC-HEADER-14: parse the frontmatter block as the user wrote it, keeping
28
- * keys the schema doesn't recognize (Zod strip-mode parsing discards them).
29
- */
30
- function extractRawFrontmatter(rawContent) {
31
- // Shared helper normalizes CRLF (SYNC-FORMAT-1) so a Windows-saved file's
32
- // custom keys survive the rewrite too (DOC-HEADER-14).
33
- const frontmatterBlock = extractFrontmatterBlock(rawContent);
34
- if (frontmatterBlock === undefined) {
35
- return undefined;
36
- }
37
- try {
38
- const parsed = YAML.parse(frontmatterBlock);
39
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
40
- return parsed;
41
- }
42
- }
43
- catch {
44
- // Unparseable frontmatter would have failed schema parsing already;
45
- // treat as no extra keys to preserve.
46
- }
47
- return undefined;
48
- }
49
- /**
50
- * DOC-HEADER-14: merge the validated (and sync-updated) metadata over the raw
51
- * frontmatter so unrecognized keys survive the rewrite while the fields the
52
- * sync owns (document ID, pulledAt, defaultPrefix, version) stay updated.
53
- */
54
- export function mergeMetadataWithRawFrontmatter(metadata, rawFrontmatter) {
55
- if (!rawFrontmatter) {
56
- return metadata;
57
- }
58
- const merged = { ...rawFrontmatter, ...metadata };
59
- const rawDocument = rawFrontmatter.document;
60
- if (metadata.document &&
61
- rawDocument &&
62
- typeof rawDocument === "object" &&
63
- !Array.isArray(rawDocument)) {
64
- merged.document = {
65
- ...rawDocument,
66
- ...metadata.document,
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
- }
75
- }
76
- return merged;
77
- }
78
25
  /**
79
26
  * Parse files for sync. Returns parsed files with metadata and content.
80
27
  *
@@ -93,10 +40,15 @@ export function parseFilesForPush(filePaths) {
93
40
  const flatRequirements = getAllRequirements(parsed.requirements);
94
41
  // Extract markdown content (everything after frontmatter)
95
42
  const markdownContent = extractMarkdownContent(rawContent);
96
- // DOC-HEADER-11.1/11.2: Infer prefix from first requirement if not specified
43
+ // DOC-HEADER-11.1/11.2: Infer prefix from first requirement if not specified.
44
+ // A file with no `document:` section at all still gets one — such files sync
45
+ // like any other, so gating inference on the section's presence would deny
46
+ // them a prefix for a reason Jaime never sees.
47
+ if (!parsed.metadata.document)
48
+ parsed.metadata.document = {};
97
49
  const doc = parsed.metadata.document;
98
50
  let inferredDefaultPrefix = false;
99
- if (doc && !doc.defaultPrefix && flatRequirements.length > 0) {
51
+ if (!doc.defaultPrefix && flatRequirements.length > 0) {
100
52
  const firstReq = flatRequirements[0];
101
53
  if (firstReq) {
102
54
  const parsedKey = parseRequirementKey(firstReq.id);
@@ -113,8 +65,6 @@ export function parseFilesForPush(filePaths) {
113
65
  metadata: parsed.metadata,
114
66
  markdownContent,
115
67
  requirementCount: flatRequirements.length,
116
- // DOC-HEADER-14: keep the user's frontmatter (unrecognized keys included)
117
- rawFrontmatter: extractRawFrontmatter(rawContent),
118
68
  inferredDefaultPrefix,
119
69
  });
120
70
  totalRequirements += flatRequirements.length;
@@ -4,5 +4,5 @@
4
4
  * Shared parse-and-write-back primitives for syncing local requirements files
5
5
  * to the cloud, consumed by the comparator-driven sync path.
6
6
  */
7
- export { assertNoDuplicateDocumentIds, extractMarkdownContent, mergeMetadataWithRawFrontmatter, type ParsedFile, type ParseFailure, parseFilesForPush, parseFilesForPushIndividually, WEB_APP_URL, } from "./core.js";
7
+ export { assertNoDuplicateDocumentIds, extractMarkdownContent, type ParsedFile, type ParseFailure, parseFilesForPush, parseFilesForPushIndividually, WEB_APP_URL, } from "./core.js";
8
8
  //# sourceMappingURL=index.d.ts.map
@@ -6,5 +6,5 @@
6
6
  */
7
7
  export { assertNoDuplicateDocumentIds,
8
8
  // Functions
9
- extractMarkdownContent, mergeMetadataWithRawFrontmatter, parseFilesForPush, parseFilesForPushIndividually, WEB_APP_URL, } from "./core.js";
9
+ extractMarkdownContent, parseFilesForPush, parseFilesForPushIndividually, WEB_APP_URL, } from "./core.js";
10
10
  //# sourceMappingURL=index.js.map
@@ -3,6 +3,7 @@
3
3
  * coverage from dot•requirements cloud. Shared between the MCP `report`
4
4
  * tool and the CLI `report` verb's `--source cloud` mode.
5
5
  */
6
+ import { refusalReason } from "../harness/reportingStatus.js";
6
7
  /**
7
8
  * Query cloud for coverage on a single requirement, optionally filtered by
8
9
  * branch or recorded-since timestamp.
@@ -32,7 +33,9 @@ export async function getRequirementCoverage(requirementKey, projectId, projectS
32
33
  }
33
34
  const json = (await response.json());
34
35
  if (json.status === "error") {
35
- throw new Error(`Convex query failed: ${json.errorMessage}`);
36
+ // COVERAGE-REPORT-1.2: the reason lives in errorData; errorMessage is the
37
+ // string production redacts to "[Request ID: …] Server Error".
38
+ throw new Error(`Could not read cloud coverage: ${refusalReason(json)}`);
36
39
  }
37
40
  const record = json.value;
38
41
  const { branch, sinceTimestamp } = options ?? {};
@@ -80,7 +83,9 @@ export async function getProjectCoverage(projectId, projectSecret, convexUrl, op
80
83
  }
81
84
  const json = (await response.json());
82
85
  if (json.status === "error") {
83
- throw new Error(`Convex query failed: ${json.errorMessage}`);
86
+ // COVERAGE-REPORT-1.2: the reason lives in errorData; errorMessage is the
87
+ // string production redacts to "[Request ID: …] Server Error".
88
+ throw new Error(`Could not read cloud coverage: ${refusalReason(json)}`);
84
89
  }
85
90
  return json.value;
86
91
  }
@@ -6,9 +6,9 @@
6
6
  export { buildRequirementMarkdown, buildRequirementsMarkdown, DEFAULT_DELIMITER, } from "./builder.js";
7
7
  export type { ConvexRequirement } from "./conversions.js";
8
8
  export { buildMetadata, constructKey, convexToRequirements, extractRequirementKeys, groupByRoot, parseKey, requirementsToConvex, } from "./conversions.js";
9
- export { DELIMITER_PATTERN, extractRequirementBlocks, findRequirementById, flattenRequirementTree, getAllRequirements, parseCriterionLine, parseRequirementBlock, parseRequirementBlocksFromMarkdown, parseRootLine, ROOT_LABEL_MARKER, } from "./parser-core.js";
9
+ export { DELIMITER_PATTERN, type ExtractedRequirementBlock, extractRequirementBlocks, findRequirementById, flattenRequirementTree, getAllRequirements, parseCriterionLine, parseRequirementBlock, parseRequirementBlocksFromMarkdown, parseRootLine, ROOT_LABEL_MARKER, splitRequirementFenceContent, } from "./parser-core.js";
10
10
  export type { Metadata, ParsedCriterion, RequirementKey, RequirementNode, RequirementPrefix, RequirementsFile, } from "./schemas.js";
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";
11
+ export { buildRequirementKey, canonicalRequirementKey, MetadataSchema, normalizePrefix, ParsedCriterionSchema, parseRequirementKey, REQUIREMENT_KEY_PATTERN, REQUIREMENT_PREFIX_PATTERN, RequirementKeySchema, RequirementNodeSchema, RequirementPrefixSchema, RequirementsFileSchema, ValidationError, validateKey, validateMetadata, validatePrefix, validateRequirementNode, validateRequirementsFile, } from "./schemas.js";
12
12
  export { composeMarkdownWithTitle, type SplitMarkdown, splitLeadingH1, } from "./title-markdown.js";
13
13
  export type { BuildScenarioOptions, Scenario, ScenarioAssertionSource, ScenarioStep, } from "./scenario.js";
14
14
  export { buildScenarioFromRequirements, requirementTreeToScenario, } from "./scenario.js";
@@ -11,9 +11,9 @@ export { buildMetadata, constructKey, convexToRequirements, extractRequirementKe
11
11
  export { DELIMITER_PATTERN, extractRequirementBlocks, findRequirementById, flattenRequirementTree, getAllRequirements, parseCriterionLine, parseRequirementBlock, parseRequirementBlocksFromMarkdown, parseRootLine,
12
12
  // The web reaches parser-core only through this entry point, so without this
13
13
  // its readers have no way to import the marker and must hard-code it.
14
- ROOT_LABEL_MARKER, } from "./parser-core.js";
14
+ ROOT_LABEL_MARKER, splitRequirementFenceContent, } from "./parser-core.js";
15
15
  // Schemas and types (uses zod - browser-safe)
16
- export { buildRequirementKey,
16
+ export { buildRequirementKey, canonicalRequirementKey,
17
17
  // Zod schemas
18
18
  MetadataSchema,
19
19
  // Prefix/key utilities
@@ -17,7 +17,12 @@ export declare function buildRequirementsMarkdown(metadata: Metadata, requiremen
17
17
  export declare function buildRequirementMarkdown(node: RequirementNode): string;
18
18
  /**
19
19
  * Build a complete requirements file from metadata and raw markdown content.
20
- * Used by CLI pull to combine frontmatter with cloud-sourced markdown.
20
+ *
21
+ * Builds frontmatter from the recognized subset only, so it is for composing a
22
+ * file from scratch — NOT for rewriting one that exists. Rewriting through here
23
+ * drops the fields, comments and formatting its author added; sync rewrites go
24
+ * through `writeRequirementsFile`, which edits the file's own frontmatter
25
+ * (DOC-HEADER-14).
21
26
  *
22
27
  * SYNC-ARCH-1: markdownContent is the source of truth from the cloud.
23
28
  */
@@ -96,7 +96,12 @@ export function buildRequirementMarkdown(node) {
96
96
  }
97
97
  /**
98
98
  * Build a complete requirements file from metadata and raw markdown content.
99
- * Used by CLI pull to combine frontmatter with cloud-sourced markdown.
99
+ *
100
+ * Builds frontmatter from the recognized subset only, so it is for composing a
101
+ * file from scratch — NOT for rewriting one that exists. Rewriting through here
102
+ * drops the fields, comments and formatting its author added; sync rewrites go
103
+ * through `writeRequirementsFile`, which edits the file's own frontmatter
104
+ * (DOC-HEADER-14).
100
105
  *
101
106
  * SYNC-ARCH-1: markdownContent is the source of truth from the cloud.
102
107
  */
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Convert between Convex flat representation and hierarchical RequirementNode trees.
3
3
  */
4
- import { REQUIREMENT_KEY_PATTERN, } from "./schemas.js";
4
+ import { canonicalRequirementKey, REQUIREMENT_KEY_PATTERN, } from "./schemas.js";
5
5
  /**
6
6
  * Construct a requirement key from prefix and index.
7
7
  * E.g., ("REQ", 123) -> "REQ-123"
@@ -140,8 +140,17 @@ function flattenNode(node, projectId, rootId, position, result, documentId) {
140
140
  index = node.metadata.index;
141
141
  }
142
142
  else {
143
- // Parse from node.id (e.g., "REQ-123" -> { prefix: "REQ", index: 123 })
144
- const parsed = parseKey(node.id);
143
+ // Parse from node.id (e.g., "REQ-123" -> { prefix: "REQ", index: 123 }).
144
+ // Only a root's id is a key, so only a root's id is normalized, so authored
145
+ // case and leading zeros reach the cloud canonically (SYNC-KEY-1.3). A
146
+ // child's id is a position path ("REQ-123.0"), which is not a key.
147
+ //
148
+ // Normalize without validating: this is exported API handed trees from
149
+ // YAML-era files and cloud rows, so an id that cannot be canonicalized must
150
+ // degrade to the prefix=node.id path below, not throw.
151
+ const parsed = isRoot
152
+ ? parseKey(canonicalRequirementKey(node.id))
153
+ : parseKey(node.id);
145
154
  if (parsed) {
146
155
  prefix = parsed.prefix;
147
156
  index = parsed.index;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The one place a sync replaces the bytes of a `.requirements.md` file.
3
+ * (codebase-to-spec authors files of its own; it does not write through here.)
4
+ *
5
+ * Both sync directions write through this, so the frontmatter a user authored
6
+ * survives whichever side won the reconciliation (DOC-HEADER-14). The write
7
+ * edits the file's own frontmatter in place and overwrites only the fields sync
8
+ * owns — which is why this takes an `OwnedFrontmatter` and not a `Metadata`.
9
+ * `Metadata` means "the recognized subset", so building one to write is
10
+ * silently the act of discarding everything else.
11
+ */
12
+ /**
13
+ * What this write does to the file's `defaultPrefix`.
14
+ *
15
+ * A cloud document with no prefix is not self-interpreting, so the caller says
16
+ * which reading applies rather than passing an absent value and letting the
17
+ * write guess: under an additive sync the cloud has no prefix to impose
18
+ * (DOC-HEADER-11.6), while under `--cloud-wins` the cloud is the authority and
19
+ * its silence is the answer (DOC-HEADER-11.7).
20
+ */
21
+ export type PrefixDirective =
22
+ /** The cloud has a prefix; the file takes it. */
23
+ {
24
+ set: string;
25
+ }
26
+ /** Leave whatever the file says — the prefix is Jaime's until the cloud has one. */
27
+ | "keep"
28
+ /** Remove it — the authoritative side has no prefix. */
29
+ | "clear";
30
+ /**
31
+ * The frontmatter fields sync owns and rewrites on every write
32
+ * (DOC-HEADER-14.1). `pulledAt` is owned too but never passed: it always
33
+ * records this write (SYNC-WRITE-1.1).
34
+ */
35
+ export interface OwnedFrontmatter {
36
+ /** The cloud document this file is linked to. */
37
+ documentId: string;
38
+ /** Editor hint for new requirement keys — see PrefixDirective. */
39
+ defaultPrefix: PrefixDirective;
40
+ /** The cloud version this content is at. */
41
+ version: number;
42
+ }
43
+ /**
44
+ * Write `body` to `filePath` under the file's own frontmatter, with the owned
45
+ * fields updated. Returns true when the bytes actually changed — sync only
46
+ * touches files whose content changed (SYNC-WRITE-1.0).
47
+ */
48
+ export declare function writeRequirementsFile(filePath: string, owned: OwnedFrontmatter, body: string): boolean;
49
+ //# sourceMappingURL=file-writer.d.ts.map
@@ -0,0 +1,138 @@
1
+ /**
2
+ * The one place a sync replaces the bytes of a `.requirements.md` file.
3
+ * (codebase-to-spec authors files of its own; it does not write through here.)
4
+ *
5
+ * Both sync directions write through this, so the frontmatter a user authored
6
+ * survives whichever side won the reconciliation (DOC-HEADER-14). The write
7
+ * edits the file's own frontmatter in place and overwrites only the fields sync
8
+ * owns — which is why this takes an `OwnedFrontmatter` and not a `Metadata`.
9
+ * `Metadata` means "the recognized subset", so building one to write is
10
+ * silently the act of discarding everything else.
11
+ */
12
+ import * as fs from "node:fs";
13
+ import YAML from "yaml";
14
+ import { extractFrontmatterBlock } from "./parser-core.js";
15
+ /**
16
+ * `lineWidth`/`minContentWidth`: never fold a long value onto continuation
17
+ * lines. `flowCollectionPadding`: re-render `tags: [auth, login]` as written,
18
+ * rather than respacing the author's line to `[ auth, login ]`.
19
+ */
20
+ const RENDER_OPTIONS = {
21
+ lineWidth: 0,
22
+ minContentWidth: 0,
23
+ flowCollectionPadding: false,
24
+ };
25
+ /**
26
+ * Write `body` to `filePath` under the file's own frontmatter, with the owned
27
+ * fields updated. Returns true when the bytes actually changed — sync only
28
+ * touches files whose content changed (SYNC-WRITE-1.0).
29
+ */
30
+ export function writeRequirementsFile(filePath, owned, body) {
31
+ const current = fs.existsSync(filePath)
32
+ ? fs.readFileSync(filePath, "utf-8")
33
+ : undefined;
34
+ const frontmatter = readFrontmatter(current);
35
+ // On a file being created there is nothing to compare against, so stamp it
36
+ // now and let `pulledAt` lead the frontmatter as it does everywhere else.
37
+ if (current === undefined)
38
+ frontmatter.setIn(["pulledAt"], nowStamp());
39
+ frontmatter.setIn(["version"], owned.version);
40
+ ensureDocumentMap(frontmatter);
41
+ frontmatter.setIn(["document", "id"], owned.documentId);
42
+ if (owned.defaultPrefix === "clear")
43
+ frontmatter.deleteIn(["document", "defaultPrefix"]);
44
+ else if (owned.defaultPrefix !== "keep")
45
+ frontmatter.setIn(["document", "defaultPrefix"], owned.defaultPrefix.set);
46
+ // SYNC-TITLE-1.2: `document.title` is a recognized-but-retired field — the
47
+ // title lives in the body as its leading H1 — so a rewrite drops it rather
48
+ // than carrying it forward as if it were one of the user's own fields.
49
+ frontmatter.deleteIn(["document", "title"]);
50
+ // SYNC-WRITE-1.0: render with the file's OWN `pulledAt` still in place. If
51
+ // that already matches the file, the only thing this write would change is
52
+ // the stamp — and `pulledAt` records when content arrived, not when a sync
53
+ // ran (SYNC-WRITE-1.1) — so leave the file alone.
54
+ //
55
+ // Comparing two rendered files, rather than patching the stamp out of the
56
+ // text, is what makes this hold for any style the author used: a flow-style
57
+ // mapping (`{owner: alice, document: {...}}`) puts every key on one line, so
58
+ // a line-anchored "ignore the pulledAt line" match finds nothing and every
59
+ // sync rewrites the file.
60
+ const unstamped = compose(frontmatter, body);
61
+ // CRLF-normalize the comparison only: a Windows-authored file that hasn't
62
+ // changed keeps its line endings rather than being converted on sight.
63
+ if (current !== undefined && current.replace(/\r\n/g, "\n") === unstamped)
64
+ return false;
65
+ if (current !== undefined)
66
+ frontmatter.setIn(["pulledAt"], nowStamp());
67
+ fs.writeFileSync(filePath, compose(frontmatter, body), "utf-8");
68
+ return true;
69
+ }
70
+ const nowStamp = () => new Date().toISOString();
71
+ function compose(frontmatter, body) {
72
+ return `---\n${frontmatter.toString(RENDER_OPTIONS).trim()}\n---\n\n${body.trim()}\n`;
73
+ }
74
+ /**
75
+ * Guarantee `document:` is a mapping this write can set `id` on.
76
+ *
77
+ * A YAML alias (`document: *defaults`) parses as a valid, normally linked
78
+ * document — but editing *through* the alias would rewrite whatever its anchor
79
+ * is shared with, and a scalar or sequence cannot hold an id at all. Replace
80
+ * the node with a concrete mapping, seeded from the alias's target so nothing
81
+ * it carried is lost.
82
+ */
83
+ function ensureDocumentMap(frontmatter) {
84
+ const existing = frontmatter.getIn(["document"], true);
85
+ if (existing === undefined || YAML.isMap(existing))
86
+ return;
87
+ const resolved = YAML.isAlias(existing)
88
+ ? existing.resolve(frontmatter)
89
+ : undefined;
90
+ // `toJS(doc)`, NOT `toJSON()`: only the former resolves aliases nested INSIDE
91
+ // the target. `base: &base {owner: *n}` seeded via toJSON turns the author's
92
+ // `owner: alice` into `{source: "n"}` — silent, permanent loss of a field,
93
+ // and the write after it no-ops so the corruption becomes the stable state.
94
+ const seed = YAML.isMap(resolved) ? resolved.toJS(frontmatter) : {};
95
+ const replacement = frontmatter.createNode(seed);
96
+ // Carry any anchor the replaced node held. `document: &d hello` with a
97
+ // sibling `audit: *d` would otherwise leave that alias unresolvable and
98
+ // every render of the file would throw — a file that could never be written
99
+ // again. `*d` now resolves to the repaired mapping instead of the old
100
+ // scalar: the alias's value changes, which is the lesser loss.
101
+ const anchor = YAML.isScalar(existing) || YAML.isCollection(existing)
102
+ ? existing.anchor
103
+ : undefined;
104
+ if (anchor && YAML.isCollection(replacement))
105
+ replacement.anchor = anchor;
106
+ frontmatter.setIn(["document"], replacement);
107
+ }
108
+ /**
109
+ * The file's frontmatter as the user wrote it — unrecognized keys, comments and
110
+ * flow style included (DOC-HEADER-14.0). Editing this document in place, rather
111
+ * than rebuilding one from a parsed object, is what lets `owner: alice # rota`
112
+ * come back with its comment attached.
113
+ *
114
+ * Frontmatter that isn't a YAML mapping yields an empty document: the file is
115
+ * unparseable as a requirements file anyway, and rewriting it is the repair.
116
+ * A mapping whose `document:` key holds something unusable is repaired in
117
+ * place instead — see `ensureDocumentMap`.
118
+ */
119
+ function readFrontmatter(current) {
120
+ const empty = () => new YAML.Document({});
121
+ if (current === undefined)
122
+ return empty();
123
+ const yaml = extractFrontmatterBlock(current);
124
+ if (yaml === undefined)
125
+ return empty();
126
+ try {
127
+ // `uniqueKeys: false`: a repeated key (`owner: alice` / `owner: bob`) is
128
+ // an error by default, which would send this file down the empty-document
129
+ // path and delete every field its author wrote. Duplicates are the
130
+ // author's to resolve; losing both is not a repair.
131
+ const doc = YAML.parseDocument(yaml, { uniqueKeys: false });
132
+ return doc.errors.length === 0 && YAML.isMap(doc.contents) ? doc : empty();
133
+ }
134
+ catch {
135
+ return empty();
136
+ }
137
+ }
138
+ //# sourceMappingURL=file-writer.js.map
@@ -7,11 +7,14 @@
7
7
  export { buildRequirementMarkdown, buildRequirementsFile, buildRequirementsMarkdown, } from "./builder.js";
8
8
  export type { ConvexRequirement } from "./conversions.js";
9
9
  export { buildMetadata, constructKey, convexToRequirements, extractRequirementKeys, groupByRoot, parseKey, requirementsToConvex, } from "./conversions.js";
10
- export { findRequirementById, flattenRequirementTree, getAllRequirements, parseCriterionLine, parseRequirementBlock, parseRequirementsFile, parseRequirementsFromFile, parseRootLine, } from "./parser.js";
10
+ export type { OwnedFrontmatter, PrefixDirective } from "./file-writer.js";
11
+ export { writeRequirementsFile } from "./file-writer.js";
12
+ export type { ExtractedRequirementBlock } from "./parser.js";
13
+ export { findRequirementById, flattenRequirementTree, getAllRequirements, parseCriterionLine, parseRequirementBlock, parseRequirementsFile, parseRequirementsFromFile, parseRootLine, splitRequirementFenceContent, } from "./parser.js";
11
14
  export { checkPathAmbiguity, findChildrenByLabel, getAllLabelPaths, parsePathSegment, parseRequirementPath, resolvePathSegment, resolveRequirementPath, resolveToNumericPath, } from "./resolver.js";
12
15
  export type { BuildScenarioOptions, Scenario, ScenarioAssertionSource, ScenarioStep, } from "./scenario.js";
13
16
  export { buildScenarioFromRequirements, requirementTreeToScenario, } from "./scenario.js";
14
17
  export type { Metadata, ParsedCriterion, PushValidationResult, RequirementKey, RequirementNode, RequirementPrefix, RequirementsFile, } from "./schemas.js";
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";
18
+ export { buildRequirementKey, CONVEX_ID_PATTERN, canonicalRequirementKey, 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
19
  export { composeMarkdownWithTitle, type SplitMarkdown, splitLeadingH1, } from "./title-markdown.js";
17
20
  //# sourceMappingURL=index.d.ts.map
@@ -7,14 +7,15 @@
7
7
  // Building
8
8
  export { buildRequirementMarkdown, buildRequirementsFile, buildRequirementsMarkdown, } from "./builder.js";
9
9
  export { buildMetadata, constructKey, convexToRequirements, extractRequirementKeys, groupByRoot, parseKey, requirementsToConvex, } from "./conversions.js";
10
+ export { writeRequirementsFile } from "./file-writer.js";
10
11
  // Parsing
11
- export { findRequirementById, flattenRequirementTree, getAllRequirements, parseCriterionLine, parseRequirementBlock, parseRequirementsFile, parseRequirementsFromFile, parseRootLine, } from "./parser.js";
12
+ export { findRequirementById, flattenRequirementTree, getAllRequirements, parseCriterionLine, parseRequirementBlock, parseRequirementsFile, parseRequirementsFromFile, parseRootLine, splitRequirementFenceContent, } from "./parser.js";
12
13
  // Path resolution
13
14
  export { checkPathAmbiguity, findChildrenByLabel, getAllLabelPaths, parsePathSegment, parseRequirementPath, resolvePathSegment, resolveRequirementPath, resolveToNumericPath, } from "./resolver.js";
14
15
  // Scenario building (for browser-automation / runScenario)
15
16
  export { buildScenarioFromRequirements, requirementTreeToScenario, } from "./scenario.js";
16
17
  // Schemas and types
17
- export { buildRequirementKey, CONVEX_ID_PATTERN,
18
+ export { buildRequirementKey, CONVEX_ID_PATTERN, canonicalRequirementKey,
18
19
  // Zod schemas
19
20
  MetadataSchema,
20
21
  // Prefix/key utilities
@@ -59,15 +59,42 @@ export declare function parseRootLine(line: string, delimiter?: string): {
59
59
  * First line is the requirement content (with optional arrow format).
60
60
  * Subsequent lines are criteria with position paths.
61
61
  */
62
- export declare function parseRequirementBlock(key: string, blockContent: string): RequirementNode;
62
+ export declare function parseRequirementBlock(key: string, blockContent: string,
63
63
  /**
64
- * Extract requirement blocks from Markdown body (no frontmatter).
65
- * Returns an array of { key, blockContent } for each dotrequirements block found.
64
+ * Zero-based line index of this block within its fence. A fence may group
65
+ * several roots, so without it every root's diagnostics would count from its
66
+ * own first line and "at line 3" would occur once per root.
66
67
  */
67
- export declare function extractRequirementBlocks(body: string): Array<{
68
+ lineOffset?: number): RequirementNode;
69
+ export type ExtractedRequirementBlock = {
68
70
  key: string;
69
71
  blockContent: string;
70
- }>;
72
+ /** Zero-based line index of this root within its fence, for diagnostics. */
73
+ startLine: number;
74
+ };
75
+ /**
76
+ * Split one dotrequirements fence into its independent root requirements.
77
+ *
78
+ * A fence marks a contiguous structured region; each unindented, well-formed
79
+ * KEY: content line starts a new root. The shared key pattern is tested against
80
+ * an uppercase candidate because key validation is case-insensitive
81
+ * (SYNC-KEY-1.3). Authored casing remains untouched in the returned block.
82
+ *
83
+ * Splitting is structural, not a grammar check: it rejects content that is not
84
+ * shaped like a requirement block at all, but a *malformed key* is passed
85
+ * through for the caller to judge. Validating keys here made every reader as
86
+ * strict as the authoring path — a legacy "LOGIN_1" in the cloud took down a
87
+ * whole `dotreq diff` run and rendered as raw markdown in the chat document
88
+ * view. Key grammar is enforced in extractRequirementBlocks (SYNC-KEY-1),
89
+ * on the authoring and save paths.
90
+ */
91
+ export declare function splitRequirementFenceContent(content: string): ExtractedRequirementBlock[];
92
+ /**
93
+ * Extract requirement blocks from Markdown body (no frontmatter).
94
+ * A fence may contain multiple adjacent root requirements; each root is
95
+ * returned as its own logical block.
96
+ */
97
+ export declare function extractRequirementBlocks(body: string): ExtractedRequirementBlock[];
71
98
  /**
72
99
  * Split raw file content into its leading YAML frontmatter and body — both
73
100
  * derived from ONE fence match, so the two halves are complementary by