@popoverai/dotrequirements 0.29.1 → 0.29.3
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.js +0 -4
- package/dist/codebase-to-spec/renumber.d.ts +7 -5
- package/dist/codebase-to-spec/renumber.js +17 -15
- package/dist/commands/report.js +15 -1
- package/dist/commands/sync.js +7 -1
- package/dist/harness/cache.d.ts +83 -2
- package/dist/harness/cache.js +94 -8
- package/dist/harness/finalize.js +238 -78
- package/dist/harness/reportingStatus.d.ts +44 -0
- package/dist/harness/reportingStatus.js +123 -0
- package/dist/push/core.d.ts +0 -12
- package/dist/push/core.js +8 -58
- package/dist/push/index.d.ts +1 -1
- package/dist/push/index.js +1 -1
- package/dist/requirements/cloud-coverage.js +7 -2
- package/dist/schema/browser.d.ts +2 -2
- package/dist/schema/browser.js +2 -2
- package/dist/schema/builder.d.ts +6 -1
- package/dist/schema/builder.js +6 -1
- package/dist/schema/conversions.js +12 -3
- package/dist/schema/file-writer.d.ts +49 -0
- package/dist/schema/file-writer.js +138 -0
- package/dist/schema/index.d.ts +5 -2
- package/dist/schema/index.js +3 -2
- package/dist/schema/parser-core.d.ts +32 -5
- package/dist/schema/parser-core.js +136 -31
- package/dist/schema/parser.d.ts +2 -1
- package/dist/schema/parser.js +1 -1
- package/dist/schema/schemas.d.ts +11 -0
- package/dist/schema/schemas.js +14 -0
- package/dist/sync/compare.js +16 -1
- package/dist/sync/execute.d.ts +10 -2
- package/dist/sync/execute.js +44 -18
- package/dist/sync/local-files.d.ts +2 -12
- package/dist/sync/local-files.js +2 -62
- package/dist/sync/segment.d.ts +2 -2
- package/dist/sync/segment.js +45 -11
- package/package.json +2 -2
- package/dist/harness/convexReporting.d.ts +0 -15
- package/dist/harness/convexReporting.js +0 -131
- package/dist/harness/coverageCache.d.ts +0 -30
- 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 {
|
|
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 (
|
|
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;
|
package/dist/push/index.d.ts
CHANGED
|
@@ -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,
|
|
7
|
+
export { assertNoDuplicateDocumentIds, extractMarkdownContent, type ParsedFile, type ParseFailure, parseFilesForPush, parseFilesForPushIndividually, WEB_APP_URL, } from "./core.js";
|
|
8
8
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/push/index.js
CHANGED
|
@@ -6,5 +6,5 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export { assertNoDuplicateDocumentIds,
|
|
8
8
|
// Functions
|
|
9
|
-
extractMarkdownContent,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/schema/browser.d.ts
CHANGED
|
@@ -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";
|
package/dist/schema/browser.js
CHANGED
|
@@ -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
|
package/dist/schema/builder.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
*/
|
package/dist/schema/builder.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
|
|
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
|
package/dist/schema/index.d.ts
CHANGED
|
@@ -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 {
|
|
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
|
package/dist/schema/index.js
CHANGED
|
@@ -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
|
|
62
|
+
export declare function parseRequirementBlock(key: string, blockContent: string,
|
|
63
63
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
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
|
-
|
|
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
|