@popoverai/dotrequirements 0.29.3 → 0.30.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/commands/sync.js +22 -8
- package/dist/harness/cache.d.ts +1 -1
- package/dist/harness/cache.js +14 -0
- package/dist/harness/finalize.js +9 -8
- package/dist/harness/requirementsLoader.js +14 -3
- package/dist/requirements/coverage.js +28 -22
- package/dist/schema/browser.d.ts +1 -1
- package/dist/schema/browser.js +1 -1
- package/dist/schema/index.d.ts +2 -2
- package/dist/schema/index.js +2 -2
- package/dist/schema/resolver.d.ts +1 -1
- package/dist/schema/resolver.js +20 -2
- package/dist/schema/schemas.d.ts +8 -0
- package/dist/schema/schemas.js +13 -0
- package/dist/schema/title-markdown.d.ts +18 -0
- package/dist/schema/title-markdown.js +20 -0
- package/dist/sync/compare.d.ts +8 -0
- package/dist/sync/compare.js +29 -0
- package/dist/sync/execute.d.ts +20 -3
- package/dist/sync/execute.js +49 -16
- package/dist/sync/index.d.ts +1 -1
- package/dist/sync/index.js +1 -1
- package/dist/sync/publish.d.ts +59 -0
- package/dist/sync/publish.js +45 -0
- package/dist/sync/segment.js +5 -14
- package/package.json +10 -6
package/dist/commands/sync.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import * as path from "node:path";
|
|
8
8
|
import * as readline from "node:readline";
|
|
9
9
|
import { executePlan } from "../sync/execute.js";
|
|
10
|
-
import { acquireCloudSnapshot, acquireLocalSnapshot, compareSnapshots,
|
|
10
|
+
import { acquireCloudSnapshot, acquireLocalSnapshot, compareSnapshots, duplicateIdAbortMessage, filterByScope, resolutionHint, } from "../sync/index.js";
|
|
11
11
|
import { buildPlan, resolveMode, } from "../sync/plan.js";
|
|
12
12
|
import { brand } from "../utils/brand.js";
|
|
13
13
|
import { resolveCloudAuth } from "./sync-common.js";
|
|
@@ -60,13 +60,11 @@ export async function syncCommand(scope, options) {
|
|
|
60
60
|
console.log(`Project: "${cloud.projectName}"`);
|
|
61
61
|
}
|
|
62
62
|
// SYNC-FAIL-3: two files claiming one document id would both write it — abort
|
|
63
|
-
// before any write, naming both files and the shared id
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
throw new Error(
|
|
68
|
-
d.filePaths.map((p) => ` ${p}`).join("\n") +
|
|
69
|
-
`\nEach file must map to its own cloud document. Remove the "id:" line from the copy's frontmatter, then sync again.`);
|
|
63
|
+
// before any write, naming both files and the shared id, plus any invalid
|
|
64
|
+
// files (SYNC-FAIL-3.2) so one run reports every problem.
|
|
65
|
+
const abortMessage = duplicateIdAbortMessage(local);
|
|
66
|
+
if (abortMessage) {
|
|
67
|
+
throw new Error(abortMessage);
|
|
70
68
|
}
|
|
71
69
|
const comparison = compareSnapshots(local, cloud);
|
|
72
70
|
const { documents, unmatched } = filterByScope(comparison, scope, workspaceRoot);
|
|
@@ -134,6 +132,22 @@ export async function syncCommand(scope, options) {
|
|
|
134
132
|
console.log(` ⚠ ${name}: this file's import marker was not recognized — your CLI may need updating. Its requirements were saved as natively authored (they count toward your plan's requirement limit).`);
|
|
135
133
|
}
|
|
136
134
|
}
|
|
135
|
+
// The publish landed but something after it did not. The server's own words,
|
|
136
|
+
// because it knows what was left undone and whether it repairs itself.
|
|
137
|
+
for (const { name, warning } of outcome.publishWarnings) {
|
|
138
|
+
console.log(` ⚠ ${name}: ${warning}`);
|
|
139
|
+
}
|
|
140
|
+
// SYNC-CLI-EDIT-3.1: the cloud stores the web editor's notation, and files
|
|
141
|
+
// written another way converge to it on push. Each file NAMED, not counted —
|
|
142
|
+
// a count leaves Jaime to find out which files changed from `git diff`,
|
|
143
|
+
// which is exactly what this report exists to prevent.
|
|
144
|
+
if (outcome.normalized.length > 0) {
|
|
145
|
+
const n = outcome.normalized.length;
|
|
146
|
+
console.log(` ${n} document${n === 1 ? "" : "s"} normalized to web-compatible notation:`);
|
|
147
|
+
for (const { name } of outcome.normalized) {
|
|
148
|
+
console.log(` ${name}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
137
151
|
// SYNC-FAIL-2.1: saved to cloud, but the local file couldn't be updated —
|
|
138
152
|
// give the re-link recovery step so a retry doesn't mint a duplicate.
|
|
139
153
|
for (const w of outcome.writeBackWarnings) {
|
package/dist/harness/cache.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* - tracking.jsonl: Append-only tracking of requirement() calls (cross-process safe)
|
|
7
7
|
* - coverage.json: Deduplication cache for cloud reporting
|
|
8
8
|
*/
|
|
9
|
-
import type
|
|
9
|
+
import { type RequirementNode } from "../schema/index.js";
|
|
10
10
|
/**
|
|
11
11
|
* Tracking entry for a single requirement() call.
|
|
12
12
|
* Stored as one JSON object per line in tracking.jsonl
|
package/dist/harness/cache.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import * as fs from "node:fs";
|
|
10
10
|
import * as path from "node:path";
|
|
11
11
|
import { findRequirementsFilesSync } from "../requirements/index.js";
|
|
12
|
+
import { canonicalRequirementPath, } from "../schema/index.js";
|
|
12
13
|
// Cache directory structure
|
|
13
14
|
const CACHE_DIR = ".cache";
|
|
14
15
|
const LOOKUP_FILE = "lookup.json";
|
|
@@ -80,6 +81,19 @@ export function writeLookupCache(requirementsDir, requirements) {
|
|
|
80
81
|
if (labelPath && labelPath !== node.id) {
|
|
81
82
|
lookup.requirements[labelPath] = { ...entry, isAlias: true };
|
|
82
83
|
}
|
|
84
|
+
// HARNESS-RESOLVE-2.3: when the file spells the root key non-canonically
|
|
85
|
+
// (e.g. "login-01"), also index every path under the canonical spelling
|
|
86
|
+
// ("LOGIN-1"), so a lookup by the spelling other surfaces display resolves
|
|
87
|
+
// here too — including for the Python/Java helpers, whose only resolution
|
|
88
|
+
// medium is this cache. The entry's id keeps the authored spelling.
|
|
89
|
+
for (const authored of [node.id, labelPath]) {
|
|
90
|
+
if (!authored)
|
|
91
|
+
continue;
|
|
92
|
+
const canonicalPath = canonicalRequirementPath(authored);
|
|
93
|
+
if (canonicalPath !== authored && !lookup.requirements[canonicalPath]) {
|
|
94
|
+
lookup.requirements[canonicalPath] = { ...entry, isAlias: true };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
83
97
|
// Track label occurrences for disambiguation (e.g. given#0, given#1)
|
|
84
98
|
const labelCounts = new Map();
|
|
85
99
|
for (const child of node.children) {
|
package/dist/harness/finalize.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { execSync } from "node:child_process";
|
|
13
13
|
import { randomUUID } from "node:crypto";
|
|
14
|
+
import { canonicalRequirementPath } from "../schema/index.js";
|
|
14
15
|
import { getProjectInfo } from "../utils/project-settings.js";
|
|
15
16
|
import { cleanupTestRunId, clearCoverageReportFailure, deleteTrackingFile, findProjectRoot, findRequirementsDir, getTestRunId, needsReporting, readCoverageCache, readLookupCache, readTrackingEntries, recordCoverageReplayed, recordCoverageReported, recordCoverageReportFailure, updateCoverageCache, } from "./cache.js";
|
|
16
17
|
import { formatFailureNotice, formatUnrecordedNotice, refusalReason, } from "./reportingStatus.js";
|
|
@@ -478,17 +479,17 @@ export async function finalize(options = {}) {
|
|
|
478
479
|
// We resolve those to their canonical numeric key (e.g. "AUTH-LOGIN-1.0") so that
|
|
479
480
|
// coverage counting, display, and cloud reporting all use consistent keys.
|
|
480
481
|
if (lookup) {
|
|
481
|
-
// HARNESS-RESOLVE-2.1: label paths resolve case-insensitively
|
|
482
|
-
//
|
|
483
|
-
//
|
|
484
|
-
//
|
|
482
|
+
// HARNESS-RESOLVE-2.1/2.3: label paths resolve case-insensitively and
|
|
483
|
+
// root keys resolve across spellings (casing, leading zeros), but
|
|
484
|
+
// tracking preserves the test's original spelling while the cache stores
|
|
485
|
+
// label aliases lowercased and key aliases canonically spelled. Fold
|
|
486
|
+
// unmatched keys onto their normalized form first, so a ref like
|
|
487
|
+
// "REQ-1.Given" or "LOGIN-1.0" (for a file that writes "login-01") isn't
|
|
488
|
+
// dropped as foreign below.
|
|
485
489
|
for (const [key, trackingEntries] of Array.from(aggregated.entries())) {
|
|
486
490
|
if (lookup.requirements[key])
|
|
487
491
|
continue;
|
|
488
|
-
const
|
|
489
|
-
if (!m)
|
|
490
|
-
continue;
|
|
491
|
-
const folded = m[1] + m[2].toLowerCase();
|
|
492
|
+
const folded = canonicalRequirementPath(key);
|
|
492
493
|
if (folded !== key && lookup.requirements[folded]) {
|
|
493
494
|
const existing = aggregated.get(folded) || [];
|
|
494
495
|
aggregated.set(folded, [...existing, ...trackingEntries]);
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import * as fs from "node:fs";
|
|
11
11
|
import * as path from "node:path";
|
|
12
12
|
import { findRequirementsFilesSync } from "../requirements/index.js";
|
|
13
|
-
import { parseRequirementsFromFile, resolveRequirementPath, } from "../schema/index.js";
|
|
13
|
+
import { canonicalRequirementPath, parseRequirementsFromFile, resolveRequirementPath, } from "../schema/index.js";
|
|
14
14
|
import { findProjectRoot as findProjectRootByWalkUp, readLookupCache, } from "./cache.js";
|
|
15
15
|
// Cache: Maps full requirement IDs (e.g., "REQ-123.0.1") to RequirementNode
|
|
16
16
|
export const loadedRequirements = new Map();
|
|
@@ -64,8 +64,10 @@ function tryLoadFromCache(projectRoot) {
|
|
|
64
64
|
children: [], // Children are loaded separately by ID
|
|
65
65
|
};
|
|
66
66
|
loadedRequirements.set(id, node);
|
|
67
|
-
// If this is a root node (no dot in ID), add to trees cache
|
|
68
|
-
|
|
67
|
+
// If this is a root node (no dot in ID), add to trees cache. Aliases
|
|
68
|
+
// (label paths, canonical key spellings) stay out: the trees map holds
|
|
69
|
+
// one entry per authored root.
|
|
70
|
+
if (!id.includes(".") && !req.isAlias) {
|
|
69
71
|
requirementTrees.set(id, node);
|
|
70
72
|
}
|
|
71
73
|
}
|
|
@@ -163,6 +165,15 @@ export function getRequirement(reqId) {
|
|
|
163
165
|
return aliased;
|
|
164
166
|
}
|
|
165
167
|
}
|
|
168
|
+
// HARNESS-RESOLVE-2.3: root-key spelling is authoring latitude, so retry
|
|
169
|
+
// under the canonical spelling. The lookup cache stores a canonical-key
|
|
170
|
+
// alias for every node whose authored root spelling isn't already
|
|
171
|
+
// canonical, so in cache mode this get lands whatever spelling the file
|
|
172
|
+
// used; the fallback resolver below handles the no-cache mode.
|
|
173
|
+
const canonical = loadedRequirements.get(canonicalRequirementPath(reqId));
|
|
174
|
+
if (canonical) {
|
|
175
|
+
return canonical;
|
|
176
|
+
}
|
|
166
177
|
// If not found, try resolving label-based paths
|
|
167
178
|
// resolveRequirementPath expects an array of root requirements and the full path
|
|
168
179
|
const allTrees = Array.from(requirementTrees.values());
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import * as fs from "node:fs";
|
|
10
10
|
import * as path from "node:path";
|
|
11
11
|
import { findRequirementsDir, isCacheStale, readTrackingEntries, } from "../harness/cache.js";
|
|
12
|
+
import { canonicalRequirementPath } from "../schema/index.js";
|
|
12
13
|
export class LocalReportCacheMissingError extends Error {
|
|
13
14
|
constructor(message) {
|
|
14
15
|
super(message);
|
|
@@ -70,37 +71,42 @@ export function buildLocalReport(opts) {
|
|
|
70
71
|
};
|
|
71
72
|
}
|
|
72
73
|
function buildCoverageEntries(lookup, entries, filterKey) {
|
|
73
|
-
|
|
74
|
+
// Assign each tracked reference to the requirement that owns it: the exact
|
|
75
|
+
// spelling (primary or alias key) when the project writes it, else the
|
|
76
|
+
// canonical fold — the same preference order getRequirement uses
|
|
77
|
+
// (REPORT-LOCAL-1.5). Exact first matters: duplicate detection is
|
|
78
|
+
// per-document, so a project can legitimately hold distinct requirements
|
|
79
|
+
// whose keys differ only in spelling, and a test that referenced one must
|
|
80
|
+
// never count toward the other (HARNESS-RESOLVE-3.0). Locations group
|
|
81
|
+
// under the owner's node id, which alias entries share with their primary.
|
|
82
|
+
const locationsById = new Map();
|
|
74
83
|
for (const entry of entries) {
|
|
75
|
-
|
|
76
|
-
|
|
84
|
+
const owner = lookup.requirements[entry.requirementKey] ??
|
|
85
|
+
lookup.requirements[canonicalRequirementPath(entry.requirementKey)];
|
|
86
|
+
if (!owner)
|
|
87
|
+
continue;
|
|
88
|
+
if (!locationsById.has(owner.id)) {
|
|
89
|
+
locationsById.set(owner.id, new Set());
|
|
77
90
|
}
|
|
78
|
-
|
|
91
|
+
locationsById.get(owner.id).add(entry.callerLocation);
|
|
92
|
+
}
|
|
93
|
+
// The filter names one requirement; resolve it with the same preference
|
|
94
|
+
// order, then scope by the owner's node id (REPORT-LOCAL-1.3). A filter
|
|
95
|
+
// that resolves to nothing matches nothing, as before.
|
|
96
|
+
let filterId;
|
|
97
|
+
if (filterKey) {
|
|
98
|
+
const owner = lookup.requirements[filterKey] ??
|
|
99
|
+
lookup.requirements[canonicalRequirementPath(filterKey)];
|
|
100
|
+
filterId = owner?.id ?? filterKey;
|
|
79
101
|
}
|
|
80
102
|
const results = [];
|
|
81
103
|
for (const [key, req] of Object.entries(lookup.requirements)) {
|
|
82
104
|
if (req.isAlias)
|
|
83
105
|
continue;
|
|
84
|
-
if (
|
|
106
|
+
if (filterId && key !== filterId && !key.startsWith(`${filterId}.`)) {
|
|
85
107
|
continue;
|
|
86
108
|
}
|
|
87
|
-
const locations = new Set();
|
|
88
|
-
const keyLocations = locationsByKey.get(key);
|
|
89
|
-
if (keyLocations) {
|
|
90
|
-
keyLocations.forEach((l) => {
|
|
91
|
-
locations.add(l);
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
for (const [aliasKey, aliasReq] of Object.entries(lookup.requirements)) {
|
|
95
|
-
if (aliasReq.isAlias && aliasReq.id === req.id) {
|
|
96
|
-
const aliasLocations = locationsByKey.get(aliasKey);
|
|
97
|
-
if (aliasLocations) {
|
|
98
|
-
aliasLocations.forEach((l) => {
|
|
99
|
-
locations.add(l);
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
109
|
+
const locations = locationsById.get(req.id) ?? new Set();
|
|
104
110
|
results.push({
|
|
105
111
|
key,
|
|
106
112
|
label: req.label || null,
|
package/dist/schema/browser.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export { buildMetadata, constructKey, convexToRequirements, extractRequirementKe
|
|
|
9
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
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
|
-
export { composeMarkdownWithTitle, type SplitMarkdown, splitLeadingH1, } from "./title-markdown.js";
|
|
12
|
+
export { composeMarkdownWithTitle, effectiveTitle, 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";
|
|
15
15
|
//# sourceMappingURL=browser.d.ts.map
|
package/dist/schema/browser.js
CHANGED
|
@@ -24,7 +24,7 @@ REQUIREMENT_PREFIX_PATTERN, RequirementKeySchema, RequirementNodeSchema, Require
|
|
|
24
24
|
ValidationError, validateKey, validateMetadata, validatePrefix, validateRequirementNode, validateRequirementsFile, } from "./schemas.js";
|
|
25
25
|
// Title semantics (pure TypeScript - browser-safe): the document title and
|
|
26
26
|
// the markdown's leading H1 are the same thing (DOC-TITLE-3)
|
|
27
|
-
export { composeMarkdownWithTitle, splitLeadingH1, } from "./title-markdown.js";
|
|
27
|
+
export { composeMarkdownWithTitle, effectiveTitle, splitLeadingH1, } from "./title-markdown.js";
|
|
28
28
|
// Scenario building (pure TypeScript - browser-safe, used by Convex Node actions)
|
|
29
29
|
export { buildScenarioFromRequirements, requirementTreeToScenario, } from "./scenario.js";
|
|
30
30
|
//# sourceMappingURL=browser.js.map
|
package/dist/schema/index.d.ts
CHANGED
|
@@ -15,6 +15,6 @@ export { checkPathAmbiguity, findChildrenByLabel, getAllLabelPaths, parsePathSeg
|
|
|
15
15
|
export type { BuildScenarioOptions, Scenario, ScenarioAssertionSource, ScenarioStep, } from "./scenario.js";
|
|
16
16
|
export { buildScenarioFromRequirements, requirementTreeToScenario, } from "./scenario.js";
|
|
17
17
|
export type { Metadata, ParsedCriterion, PushValidationResult, RequirementKey, RequirementNode, RequirementPrefix, RequirementsFile, } 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";
|
|
19
|
-
export { composeMarkdownWithTitle, type SplitMarkdown, splitLeadingH1, } from "./title-markdown.js";
|
|
18
|
+
export { buildRequirementKey, CONVEX_ID_PATTERN, canonicalRequirementKey, canonicalRequirementPath, MetadataSchema, normalizePrefix, ParsedCriterionSchema, parseRequirementKey, REQUIREMENT_KEY_PATTERN, REQUIREMENT_PREFIX_PATTERN, RequirementKeySchema, RequirementNodeSchema, RequirementPrefixSchema, RequirementsFileSchema, ValidationError, validateForPush, validateKey, validateMetadata, validatePrefix, validateRequirementNode, validateRequirementsFile, } from "./schemas.js";
|
|
19
|
+
export { composeMarkdownWithTitle, effectiveTitle, type SplitMarkdown, splitLeadingH1, } from "./title-markdown.js";
|
|
20
20
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/schema/index.js
CHANGED
|
@@ -15,7 +15,7 @@ export { checkPathAmbiguity, findChildrenByLabel, getAllLabelPaths, parsePathSeg
|
|
|
15
15
|
// Scenario building (for browser-automation / runScenario)
|
|
16
16
|
export { buildScenarioFromRequirements, requirementTreeToScenario, } from "./scenario.js";
|
|
17
17
|
// Schemas and types
|
|
18
|
-
export { buildRequirementKey, CONVEX_ID_PATTERN, canonicalRequirementKey,
|
|
18
|
+
export { buildRequirementKey, CONVEX_ID_PATTERN, canonicalRequirementKey, canonicalRequirementPath,
|
|
19
19
|
// Zod schemas
|
|
20
20
|
MetadataSchema,
|
|
21
21
|
// Prefix/key utilities
|
|
@@ -24,5 +24,5 @@ normalizePrefix, ParsedCriterionSchema, parseRequirementKey, REQUIREMENT_KEY_PAT
|
|
|
24
24
|
REQUIREMENT_PREFIX_PATTERN, RequirementKeySchema, RequirementNodeSchema, RequirementPrefixSchema, RequirementsFileSchema,
|
|
25
25
|
// Validation
|
|
26
26
|
ValidationError, validateForPush, validateKey, validateMetadata, validatePrefix, validateRequirementNode, validateRequirementsFile, } from "./schemas.js";
|
|
27
|
-
export { composeMarkdownWithTitle, splitLeadingH1, } from "./title-markdown.js";
|
|
27
|
+
export { composeMarkdownWithTitle, effectiveTitle, splitLeadingH1, } from "./title-markdown.js";
|
|
28
28
|
//# sourceMappingURL=index.js.map
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Resolve label-based requirement paths to numeric paths.
|
|
3
3
|
* Supports: REQ123.given, REQ123.when.then, REQ123.given#1
|
|
4
4
|
*/
|
|
5
|
-
import type
|
|
5
|
+
import { type RequirementNode } from "./schemas.js";
|
|
6
6
|
/**
|
|
7
7
|
* Parse a requirement path into segments.
|
|
8
8
|
* E.g., "REQ123.given.then" → ["REQ123", "given", "then"]
|
package/dist/schema/resolver.js
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
* Resolve label-based requirement paths to numeric paths.
|
|
3
3
|
* Supports: REQ123.given, REQ123.when.then, REQ123.given#1
|
|
4
4
|
*/
|
|
5
|
+
import { canonicalRequirementKey } from "./schemas.js";
|
|
6
|
+
/**
|
|
7
|
+
* Match a path's root segment against the loaded roots: the exact spelling
|
|
8
|
+
* when a root writes it, else canonically — casing and leading zeros are
|
|
9
|
+
* authoring latitude, so `LOGIN-1` finds a root written `login-01` and vice
|
|
10
|
+
* versa (HARNESS-RESOLVE-2.3). Exact first matters: duplicate detection is
|
|
11
|
+
* per-document, so a project can legitimately hold distinct roots whose keys
|
|
12
|
+
* differ only in spelling, and each path must bind to the root that writes
|
|
13
|
+
* it exactly (HARNESS-RESOLVE-3.0).
|
|
14
|
+
*/
|
|
15
|
+
function findRoot(requirements, rootId) {
|
|
16
|
+
const exact = requirements.find((r) => r.id === rootId);
|
|
17
|
+
if (exact) {
|
|
18
|
+
return exact;
|
|
19
|
+
}
|
|
20
|
+
const canonical = canonicalRequirementKey(rootId);
|
|
21
|
+
return requirements.find((r) => canonicalRequirementKey(r.id) === canonical);
|
|
22
|
+
}
|
|
5
23
|
/**
|
|
6
24
|
* Parse a requirement path into segments.
|
|
7
25
|
* E.g., "REQ123.given.then" → ["REQ123", "given", "then"]
|
|
@@ -104,7 +122,7 @@ export function resolveRequirementPath(requirements, path) {
|
|
|
104
122
|
}
|
|
105
123
|
// First segment should be the root requirement ID
|
|
106
124
|
const rootId = segments[0];
|
|
107
|
-
let currentNode = requirements
|
|
125
|
+
let currentNode = findRoot(requirements, rootId);
|
|
108
126
|
if (!currentNode) {
|
|
109
127
|
return null;
|
|
110
128
|
}
|
|
@@ -158,7 +176,7 @@ export function checkPathAmbiguity(requirements, path) {
|
|
|
158
176
|
return 0;
|
|
159
177
|
}
|
|
160
178
|
const rootId = segments[0];
|
|
161
|
-
let currentNode = requirements
|
|
179
|
+
let currentNode = findRoot(requirements, rootId);
|
|
162
180
|
if (!currentNode) {
|
|
163
181
|
return 0;
|
|
164
182
|
}
|
package/dist/schema/schemas.d.ts
CHANGED
|
@@ -60,6 +60,14 @@ export declare function parseRequirementKey(key: string): {
|
|
|
60
60
|
* legacy malformed key rather than losing it.
|
|
61
61
|
*/
|
|
62
62
|
export declare function canonicalRequirementKey(key: string): string;
|
|
63
|
+
/**
|
|
64
|
+
* Normalize a full requirement path for comparison: canonical root key plus
|
|
65
|
+
* the path tail lowercased (label matching is case-insensitive,
|
|
66
|
+
* HARNESS-RESOLVE-2.1; root spelling is authoring latitude,
|
|
67
|
+
* HARNESS-RESOLVE-2.3). Comparison only — never a spelling to display or
|
|
68
|
+
* store in place of what the author wrote.
|
|
69
|
+
*/
|
|
70
|
+
export declare function canonicalRequirementPath(path: string): string;
|
|
63
71
|
/**
|
|
64
72
|
* Build a requirement key from prefix and number.
|
|
65
73
|
*/
|
package/dist/schema/schemas.js
CHANGED
|
@@ -103,6 +103,19 @@ export function canonicalRequirementKey(key) {
|
|
|
103
103
|
const parsed = parseRequirementKey(key);
|
|
104
104
|
return parsed ? `${parsed.prefix}-${parsed.number}` : key.toUpperCase();
|
|
105
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Normalize a full requirement path for comparison: canonical root key plus
|
|
108
|
+
* the path tail lowercased (label matching is case-insensitive,
|
|
109
|
+
* HARNESS-RESOLVE-2.1; root spelling is authoring latitude,
|
|
110
|
+
* HARNESS-RESOLVE-2.3). Comparison only — never a spelling to display or
|
|
111
|
+
* store in place of what the author wrote.
|
|
112
|
+
*/
|
|
113
|
+
export function canonicalRequirementPath(path) {
|
|
114
|
+
const dot = path.indexOf(".");
|
|
115
|
+
if (dot === -1)
|
|
116
|
+
return canonicalRequirementKey(path);
|
|
117
|
+
return (canonicalRequirementKey(path.slice(0, dot)) + path.slice(dot).toLowerCase());
|
|
118
|
+
}
|
|
106
119
|
/**
|
|
107
120
|
* Build a requirement key from prefix and number.
|
|
108
121
|
*/
|
|
@@ -27,4 +27,22 @@ export declare function splitLeadingH1(markdown: string): SplitMarkdown;
|
|
|
27
27
|
* gets no empty heading line (DOC-TITLE-3.3).
|
|
28
28
|
*/
|
|
29
29
|
export declare function composeMarkdownWithTitle(title: string, body: string): string;
|
|
30
|
+
/**
|
|
31
|
+
* The title a document currently answers to, from its row and published copy.
|
|
32
|
+
*
|
|
33
|
+
* One precedence, exported: the working title wins; else the published
|
|
34
|
+
* markdown's leading H1; else the stored title column; else untitled. The
|
|
35
|
+
* middle term is the one that keeps getting dropped when this rule is
|
|
36
|
+
* hand-copied — legacy web-authored documents carry their title only in the
|
|
37
|
+
* column, H1-authored ones only in the markdown, and every surface that
|
|
38
|
+
* composes or matches markdown (the edit path's `old_string`, the assistant's
|
|
39
|
+
* read, publish) must answer identically or a rename lands as a duplicate
|
|
40
|
+
* body heading. This was the FOURTH hand-written copy of the rule when it was
|
|
41
|
+
* extracted; the count is the argument for calling this instead of writing a
|
|
42
|
+
* fifth.
|
|
43
|
+
*/
|
|
44
|
+
export declare function effectiveTitle(workingTitle: string | undefined, published: {
|
|
45
|
+
markdownContent: string;
|
|
46
|
+
title?: string;
|
|
47
|
+
} | null | undefined): string;
|
|
30
48
|
//# sourceMappingURL=title-markdown.d.ts.map
|
|
@@ -35,4 +35,24 @@ export function composeMarkdownWithTitle(title, body) {
|
|
|
35
35
|
return `# ${trimmedTitle}\n`;
|
|
36
36
|
return `# ${trimmedTitle}\n\n${body}`;
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* The title a document currently answers to, from its row and published copy.
|
|
40
|
+
*
|
|
41
|
+
* One precedence, exported: the working title wins; else the published
|
|
42
|
+
* markdown's leading H1; else the stored title column; else untitled. The
|
|
43
|
+
* middle term is the one that keeps getting dropped when this rule is
|
|
44
|
+
* hand-copied — legacy web-authored documents carry their title only in the
|
|
45
|
+
* column, H1-authored ones only in the markdown, and every surface that
|
|
46
|
+
* composes or matches markdown (the edit path's `old_string`, the assistant's
|
|
47
|
+
* read, publish) must answer identically or a rename lands as a duplicate
|
|
48
|
+
* body heading. This was the FOURTH hand-written copy of the rule when it was
|
|
49
|
+
* extracted; the count is the argument for calling this instead of writing a
|
|
50
|
+
* fifth.
|
|
51
|
+
*/
|
|
52
|
+
export function effectiveTitle(workingTitle, published) {
|
|
53
|
+
return (workingTitle ??
|
|
54
|
+
splitLeadingH1(published?.markdownContent ?? "").title ??
|
|
55
|
+
published?.title ??
|
|
56
|
+
"");
|
|
57
|
+
}
|
|
38
58
|
//# sourceMappingURL=title-markdown.js.map
|
package/dist/sync/compare.d.ts
CHANGED
|
@@ -14,6 +14,14 @@ export declare function duplicateLocalDocumentIds(local: LocalSnapshot): Array<{
|
|
|
14
14
|
documentId: string;
|
|
15
15
|
filePaths: string[];
|
|
16
16
|
}>;
|
|
17
|
+
/**
|
|
18
|
+
* The message for the duplicate-id abort, or null when there is nothing to
|
|
19
|
+
* abort over. Built here, beside the detector, so the abort also names the
|
|
20
|
+
* snapshot's invalid files (SYNC-FAIL-3.2) — a caller that throws on the
|
|
21
|
+
* duplicate would otherwise sit on that knowledge until the next run, when
|
|
22
|
+
* the user has fixed the id and syncs again only to hit the second problem.
|
|
23
|
+
*/
|
|
24
|
+
export declare function duplicateIdAbortMessage(local: LocalSnapshot): string | null;
|
|
17
25
|
/**
|
|
18
26
|
* Compare the whole repo against the whole cloud, pairing documents by id.
|
|
19
27
|
*/
|
package/dist/sync/compare.js
CHANGED
|
@@ -247,6 +247,35 @@ export function duplicateLocalDocumentIds(local) {
|
|
|
247
247
|
.filter(([, paths]) => paths.length > 1)
|
|
248
248
|
.map(([documentId, filePaths]) => ({ documentId, filePaths }));
|
|
249
249
|
}
|
|
250
|
+
/**
|
|
251
|
+
* The message for the duplicate-id abort, or null when there is nothing to
|
|
252
|
+
* abort over. Built here, beside the detector, so the abort also names the
|
|
253
|
+
* snapshot's invalid files (SYNC-FAIL-3.2) — a caller that throws on the
|
|
254
|
+
* duplicate would otherwise sit on that knowledge until the next run, when
|
|
255
|
+
* the user has fixed the id and syncs again only to hit the second problem.
|
|
256
|
+
*/
|
|
257
|
+
export function duplicateIdAbortMessage(local) {
|
|
258
|
+
const dupes = duplicateLocalDocumentIds(local);
|
|
259
|
+
if (dupes.length === 0)
|
|
260
|
+
return null;
|
|
261
|
+
const lines = [];
|
|
262
|
+
for (const d of dupes) {
|
|
263
|
+
// Copying a starter spec twice leaves three claimants, so count them.
|
|
264
|
+
lines.push(`${d.filePaths.length} files claim the same document ID "${d.documentId}":`);
|
|
265
|
+
for (const p of d.filePaths)
|
|
266
|
+
lines.push(` ${p}`);
|
|
267
|
+
}
|
|
268
|
+
lines.push(`Each file must map to its own cloud document. Remove the "id:" line from all but one file's frontmatter, then sync again.`);
|
|
269
|
+
const invalid = local.documents.filter((doc) => doc.parseError);
|
|
270
|
+
if (invalid.length > 0) {
|
|
271
|
+
lines.push("");
|
|
272
|
+
lines.push("These files are also invalid — fix them in the same pass:");
|
|
273
|
+
for (const doc of invalid) {
|
|
274
|
+
lines.push(` ${doc.filePath} — ${doc.parseError}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return lines.join("\n");
|
|
278
|
+
}
|
|
250
279
|
/**
|
|
251
280
|
* Compare the whole repo against the whole cloud, pairing documents by id.
|
|
252
281
|
*/
|
package/dist/sync/execute.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Executing a sync plan: cloud writes/deletes and local writes/deletes.
|
|
3
3
|
*
|
|
4
|
-
* Cloud writes reuse the push machinery (parse →
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Cloud writes reuse the push machinery (parse → publish → frontmatter
|
|
5
|
+
* write-back) so document-header/import-provenance fidelity is identical to a
|
|
6
|
+
* repo→cloud push. Local writes reuse local-files.ts. Deletes still go straight
|
|
7
|
+
* to Convex: removing a document needs no conversion, so it has no reason to
|
|
8
|
+
* take the web-app hop that publishing does.
|
|
7
9
|
*/
|
|
8
10
|
import type { PlannedAction, SyncMode } from "./plan.js";
|
|
9
11
|
import type { CloudAuth } from "./snapshot.js";
|
|
@@ -38,6 +40,21 @@ export interface SyncOutcome {
|
|
|
38
40
|
name: string;
|
|
39
41
|
status: string;
|
|
40
42
|
}>;
|
|
43
|
+
/** Pushes the server accepted while telling us something about them did not
|
|
44
|
+
* finish. It answers 200 for these on purpose — the publish landed, and
|
|
45
|
+
* failing the push would send Jaime to retry a thing that already happened —
|
|
46
|
+
* so the only way they reach anyone is by being reported here. */
|
|
47
|
+
publishWarnings: Array<{
|
|
48
|
+
name: string;
|
|
49
|
+
warning: string;
|
|
50
|
+
}>;
|
|
51
|
+
/** SYNC-CLI-EDIT-3.1: files whose notation the cloud changed on storing —
|
|
52
|
+
* list markers, emphasis characters, spacing. Counted so the sync can say
|
|
53
|
+
* the reformatting happened and why, instead of leaving Jaime to discover
|
|
54
|
+
* rewritten files in `git diff`. */
|
|
55
|
+
normalized: Array<{
|
|
56
|
+
name: string;
|
|
57
|
+
}>;
|
|
41
58
|
/** SYNC-TITLE-1.3: pushes that changed a document's title. */
|
|
42
59
|
renames: Array<{
|
|
43
60
|
from: string;
|
package/dist/sync/execute.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Executing a sync plan: cloud writes/deletes and local writes/deletes.
|
|
3
3
|
*
|
|
4
|
-
* Cloud writes reuse the push machinery (parse →
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Cloud writes reuse the push machinery (parse → publish → frontmatter
|
|
5
|
+
* write-back) so document-header/import-provenance fidelity is identical to a
|
|
6
|
+
* repo→cloud push. Local writes reuse local-files.ts. Deletes still go straight
|
|
7
|
+
* to Convex: removing a document needs no conversion, so it has no reason to
|
|
8
|
+
* take the web-app hop that publishing does.
|
|
7
9
|
*/
|
|
8
10
|
import * as fs from "node:fs";
|
|
9
11
|
import * as path from "node:path";
|
|
@@ -13,6 +15,7 @@ import { api } from "../convex.js";
|
|
|
13
15
|
import { parseFilesForPushIndividually, WEB_APP_URL } from "../push/core.js";
|
|
14
16
|
import { composeMarkdownWithTitle, splitLeadingH1, writeRequirementsFile, } from "../schema/index.js";
|
|
15
17
|
import { resolveLocalPath } from "./local-files.js";
|
|
18
|
+
import { publishToCloud } from "./publish.js";
|
|
16
19
|
function emptyOutcome() {
|
|
17
20
|
return {
|
|
18
21
|
cloudCreated: 0,
|
|
@@ -26,6 +29,8 @@ function emptyOutcome() {
|
|
|
26
29
|
synced: [],
|
|
27
30
|
writeBackWarnings: [],
|
|
28
31
|
importWarnings: [],
|
|
32
|
+
publishWarnings: [],
|
|
33
|
+
normalized: [],
|
|
29
34
|
renames: [],
|
|
30
35
|
inferredPrefixes: [],
|
|
31
36
|
};
|
|
@@ -54,7 +59,7 @@ export async function executePlan(plan, cloud, auth, workspaceRoot, mode) {
|
|
|
54
59
|
outcome.invalid.push(doc);
|
|
55
60
|
break;
|
|
56
61
|
case "cloud_write":
|
|
57
|
-
await cloudWrite(
|
|
62
|
+
await cloudWrite(auth, doc, cloudById, outcome);
|
|
58
63
|
break;
|
|
59
64
|
case "cloud_delete":
|
|
60
65
|
await cloudDelete(client, auth, doc, outcome);
|
|
@@ -84,7 +89,7 @@ function errorDisplayMessage(err) {
|
|
|
84
89
|
return data.message;
|
|
85
90
|
return err instanceof Error ? err.message : String(err);
|
|
86
91
|
}
|
|
87
|
-
async function cloudWrite(
|
|
92
|
+
async function cloudWrite(auth, doc, cloudById, outcome) {
|
|
88
93
|
if (!auth.projectSlug || !doc.filePath) {
|
|
89
94
|
throw new Error("Cannot write to the cloud without project credentials.");
|
|
90
95
|
}
|
|
@@ -104,27 +109,36 @@ async function cloudWrite(client, auth, doc, cloudById, outcome) {
|
|
|
104
109
|
if (meta?.title && splitLeadingH1(parsed.markdownContent).title === null) {
|
|
105
110
|
parsed.markdownContent = composeMarkdownWithTitle(meta.title, parsed.markdownContent);
|
|
106
111
|
}
|
|
112
|
+
// SYNC-CLI-EDIT-2: the push goes to the web app, which writes it into the
|
|
113
|
+
// working document Quinn may have open and publishes a copy printed from it.
|
|
114
|
+
// Convex cannot do that job — the conversion needs the BlockNote schema.
|
|
115
|
+
//
|
|
107
116
|
// SYNC-TITLE-1: the server derives the title from the body's leading H1.
|
|
108
117
|
// A legacy frontmatter title rides along only as the server's fallback for
|
|
109
118
|
// H1-less bodies (SYNC-TITLE-1.1); the filename is never a title
|
|
110
119
|
// (SYNC-TITLE-1.0).
|
|
111
|
-
const saveResult =
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
projectSecret: auth.projectSecret,
|
|
115
|
-
},
|
|
116
|
-
target: { type: "project", slug: auth.projectSlug },
|
|
120
|
+
const saveResult = await publishToCloud({
|
|
121
|
+
projectId: auth.projectSlug,
|
|
122
|
+
projectSecret: auth.projectSecret,
|
|
117
123
|
documentId: meta?.id,
|
|
118
124
|
title: meta?.title,
|
|
119
125
|
markdownContent: parsed.markdownContent,
|
|
120
126
|
defaultPrefix: meta?.defaultPrefix,
|
|
121
127
|
runMarker: parsed.metadata.ctsRun,
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
128
|
+
});
|
|
129
|
+
const documentId = saveResult.documentId;
|
|
130
|
+
// The server accepted the publish and told us something after it did not
|
|
131
|
+
// finish. Reported rather than dropped: the push DID land, so this is not a
|
|
132
|
+
// failure, but "synced" on its own would claim more than happened.
|
|
133
|
+
if (saveResult.warning) {
|
|
134
|
+
outcome.publishWarnings.push({
|
|
135
|
+
name: path.basename(doc.filePath),
|
|
136
|
+
warning: saveResult.warning,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
125
139
|
// IMPORT-3.3: marker failure is never silent — surface the statuses the
|
|
126
140
|
// server reports as warnings.
|
|
127
|
-
if (
|
|
141
|
+
if (saveResult.importStatus) {
|
|
128
142
|
const status = saveResult.importStatus;
|
|
129
143
|
if (status === "invalid_marker" ||
|
|
130
144
|
status === "unsupported_version" ||
|
|
@@ -174,6 +188,20 @@ async function cloudWrite(client, auth, doc, cloudById, outcome) {
|
|
|
174
188
|
name: path.basename(doc.filePath),
|
|
175
189
|
url: `${WEB_APP_URL}/documents/${documentId}`,
|
|
176
190
|
});
|
|
191
|
+
// SYNC-CLI-EDIT-3: the file adopts what the cloud stored — the web editor's
|
|
192
|
+
// notation — so the repo and the cloud agree byte-for-byte and the next
|
|
193
|
+
// `dotreq diff` has nothing to say. Writing back the LOCAL bytes here left
|
|
194
|
+
// the two permanently disagreeing about spelling, which the comparator
|
|
195
|
+
// reports as a conflict on a document nobody touched. An older server does
|
|
196
|
+
// not return its stored copy; the local bytes are then exactly what it
|
|
197
|
+
// stored, so the fallback is the same agreement.
|
|
198
|
+
//
|
|
199
|
+
// `.trim()` on both sides, matching the writer's own treatment of the body —
|
|
200
|
+
// a looser comparison here reported files as normalized that the write then
|
|
201
|
+
// left untouched.
|
|
202
|
+
const contentToWriteBack = saveResult.storedMarkdown ?? parsed.markdownContent;
|
|
203
|
+
const wasNormalized = saveResult.storedMarkdown !== undefined &&
|
|
204
|
+
saveResult.storedMarkdown.trim() !== parsed.markdownContent.trim();
|
|
177
205
|
// SYNC-FAIL-2: the cloud document already exists at this point. A write-back
|
|
178
206
|
// failure is a warning on a synced document, never a sync failure (which
|
|
179
207
|
// would invite a retry that mints a duplicate).
|
|
@@ -190,7 +218,12 @@ async function cloudWrite(client, auth, doc, cloudById, outcome) {
|
|
|
190
218
|
? { set: meta.defaultPrefix }
|
|
191
219
|
: "keep",
|
|
192
220
|
version: (parsed.metadata.version ?? 0) + 1,
|
|
193
|
-
},
|
|
221
|
+
}, contentToWriteBack);
|
|
222
|
+
// Counted only once the rewrite actually happened: reporting it before a
|
|
223
|
+
// write that then failed produced two contradicting lines about one file.
|
|
224
|
+
if (wasNormalized) {
|
|
225
|
+
outcome.normalized.push({ name: path.basename(doc.filePath) });
|
|
226
|
+
}
|
|
194
227
|
}
|
|
195
228
|
catch (writeErr) {
|
|
196
229
|
outcome.writeBackWarnings.push({
|
package/dist/sync/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* (snapshot.ts), consumed by `dotreq diff` and `dotreq sync`.
|
|
4
4
|
*/
|
|
5
5
|
import type { ComparisonResult, DocumentComparison } from "./types.js";
|
|
6
|
-
export { compareSnapshots, duplicateLocalDocumentIds } from "./compare.js";
|
|
6
|
+
export { compareSnapshots, duplicateIdAbortMessage, duplicateLocalDocumentIds, } from "./compare.js";
|
|
7
7
|
export { displayName, formatUnitDetail, formatVerdictLine, resolutionHint, scopeToken, } from "./render.js";
|
|
8
8
|
export { segmentBody } from "./segment.js";
|
|
9
9
|
export { acquireCloudSnapshot, acquireLocalSnapshot, type CloudAuth, readLocalDocument, } from "./snapshot.js";
|
package/dist/sync/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* (snapshot.ts), consumed by `dotreq diff` and `dotreq sync`.
|
|
4
4
|
*/
|
|
5
5
|
import * as path from "node:path";
|
|
6
|
-
export { compareSnapshots, duplicateLocalDocumentIds } from "./compare.js";
|
|
6
|
+
export { compareSnapshots, duplicateIdAbortMessage, duplicateLocalDocumentIds, } from "./compare.js";
|
|
7
7
|
export { displayName, formatUnitDetail, formatVerdictLine, resolutionHint, scopeToken, } from "./render.js";
|
|
8
8
|
export { segmentBody } from "./segment.js";
|
|
9
9
|
export { acquireCloudSnapshot, acquireLocalSnapshot, readLocalDocument, } from "./snapshot.js";
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The push half of `dotreq sync`, posted to the web app rather than to Convex.
|
|
3
|
+
*
|
|
4
|
+
* Publishing prints the working document — the shared body the web editor is
|
|
5
|
+
* editing live — so a push has to land there first, and turning the repo's
|
|
6
|
+
* markdown into that body needs the BlockNote schema, which only the web app's
|
|
7
|
+
* runtime can build. The payload is the same one `saveWithRequirements` always
|
|
8
|
+
* took; only its destination moved.
|
|
9
|
+
*
|
|
10
|
+
* The cost is a web-app dependency in the push path: an outage there leaves the
|
|
11
|
+
* repo unable to push. Recoverable rather than lossy — see
|
|
12
|
+
* docs/working/publishing-and-unpublished-changes.md.
|
|
13
|
+
*/
|
|
14
|
+
export interface PublishParams {
|
|
15
|
+
/** Project slug or id — whichever `dotreq link` wrote into settings. */
|
|
16
|
+
projectId: string;
|
|
17
|
+
projectSecret: string;
|
|
18
|
+
/** Absent for a file that has never been pushed; the server mints the id. */
|
|
19
|
+
documentId?: string;
|
|
20
|
+
markdownContent: string;
|
|
21
|
+
/** Legacy frontmatter title, used only as the server's H1-less fallback. */
|
|
22
|
+
title?: string;
|
|
23
|
+
defaultPrefix?: string;
|
|
24
|
+
runMarker?: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* What the server reports back: the document, any run-marker complaint, and any
|
|
28
|
+
* partial success.
|
|
29
|
+
*
|
|
30
|
+
* `warning` is the server saying the publish landed but something after it did
|
|
31
|
+
* not — today, seeding a new document's working copy. It answers 200 on purpose,
|
|
32
|
+
* because the publish itself succeeded and failing the push would send Jaime to
|
|
33
|
+
* retry a thing that already happened. Dropping the warning here turned that
|
|
34
|
+
* deliberate partial success back into an unqualified one.
|
|
35
|
+
*/
|
|
36
|
+
export type PublishResult = {
|
|
37
|
+
documentId: string;
|
|
38
|
+
importStatus?: string;
|
|
39
|
+
warning?: string;
|
|
40
|
+
/**
|
|
41
|
+
* The markdown the cloud actually stored — the web editor's notation
|
|
42
|
+
* (SYNC-CLI-EDIT-3). The write-back adopts it so the file and the cloud
|
|
43
|
+
* agree byte-for-byte; without it the next `dotreq diff` reports a conflict
|
|
44
|
+
* on a document nobody touched. Absent from older servers, in which case the
|
|
45
|
+
* write-back falls back to the local bytes, which is exactly the old
|
|
46
|
+
* behaviour.
|
|
47
|
+
*/
|
|
48
|
+
storedMarkdown?: string;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* POST to `/api/publish`. Returns the saved document; throws with the server's
|
|
52
|
+
* own message on failure, which is what Jaime reads in the sync output.
|
|
53
|
+
*
|
|
54
|
+
* SYNC-CLI-EDIT-2.3: a document somebody is typing in continuously is the one
|
|
55
|
+
* refusal that is not a mistake — the message says so, and says the sync will
|
|
56
|
+
* apply next time.
|
|
57
|
+
*/
|
|
58
|
+
export declare function publishToCloud(params: PublishParams): Promise<PublishResult>;
|
|
59
|
+
//# sourceMappingURL=publish.d.ts.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The push half of `dotreq sync`, posted to the web app rather than to Convex.
|
|
3
|
+
*
|
|
4
|
+
* Publishing prints the working document — the shared body the web editor is
|
|
5
|
+
* editing live — so a push has to land there first, and turning the repo's
|
|
6
|
+
* markdown into that body needs the BlockNote schema, which only the web app's
|
|
7
|
+
* runtime can build. The payload is the same one `saveWithRequirements` always
|
|
8
|
+
* took; only its destination moved.
|
|
9
|
+
*
|
|
10
|
+
* The cost is a web-app dependency in the push path: an outage there leaves the
|
|
11
|
+
* repo unable to push. Recoverable rather than lossy — see
|
|
12
|
+
* docs/working/publishing-and-unpublished-changes.md.
|
|
13
|
+
*/
|
|
14
|
+
import { DEFAULT_API_BASE_URL } from "../requirements/cloud-ai.js";
|
|
15
|
+
/**
|
|
16
|
+
* POST to `/api/publish`. Returns the saved document; throws with the server's
|
|
17
|
+
* own message on failure, which is what Jaime reads in the sync output.
|
|
18
|
+
*
|
|
19
|
+
* SYNC-CLI-EDIT-2.3: a document somebody is typing in continuously is the one
|
|
20
|
+
* refusal that is not a mistake — the message says so, and says the sync will
|
|
21
|
+
* apply next time.
|
|
22
|
+
*/
|
|
23
|
+
export async function publishToCloud(params) {
|
|
24
|
+
const apiBaseUrl = process.env.DOTREQUIREMENTS_API_URL ?? DEFAULT_API_BASE_URL;
|
|
25
|
+
const response = await fetch(`${apiBaseUrl}/api/publish`, {
|
|
26
|
+
method: "POST",
|
|
27
|
+
headers: { "Content-Type": "application/json" },
|
|
28
|
+
body: JSON.stringify(params),
|
|
29
|
+
});
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
const body = (await response.json().catch(() => ({})));
|
|
32
|
+
throw new Error(body.error || response.statusText);
|
|
33
|
+
}
|
|
34
|
+
const { result, warning, storedMarkdown } = (await response.json());
|
|
35
|
+
const published = typeof result === "string" ? { documentId: result } : { ...result };
|
|
36
|
+
// The server's warning and stored markdown sit beside `result`, not inside
|
|
37
|
+
// it, so they have to be carried across deliberately — the warning was once
|
|
38
|
+
// destructured away here.
|
|
39
|
+
return {
|
|
40
|
+
...published,
|
|
41
|
+
...(warning !== undefined ? { warning } : {}),
|
|
42
|
+
...(storedMarkdown !== undefined ? { storedMarkdown } : {}),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=publish.js.map
|
package/dist/sync/segment.js
CHANGED
|
@@ -6,19 +6,7 @@
|
|
|
6
6
|
* normalized text (prose has no stable id, so a reworded passage reads as a
|
|
7
7
|
* removal-plus-addition — a conflict, per DIFF-4.3).
|
|
8
8
|
*/
|
|
9
|
-
import { canonicalRequirementKey, getAllRequirements, parseRequirementBlock, splitRequirementFenceContent, } from "../schema/index.js";
|
|
10
|
-
/**
|
|
11
|
-
* A node's comparison id: the canonical root key plus its position tail, so the
|
|
12
|
-
* two sides agree on identity even when they spell the key differently
|
|
13
|
-
* (DIFF-3.3). Splitting on the first "." keeps the tail untouched — position
|
|
14
|
-
* paths are already canonical.
|
|
15
|
-
*/
|
|
16
|
-
function canonicalNodeId(id) {
|
|
17
|
-
const dot = id.indexOf(".");
|
|
18
|
-
if (dot === -1)
|
|
19
|
-
return canonicalRequirementKey(id);
|
|
20
|
-
return `${canonicalRequirementKey(id.slice(0, dot))}${id.slice(dot)}`;
|
|
21
|
-
}
|
|
9
|
+
import { canonicalRequirementKey, canonicalRequirementPath, getAllRequirements, parseRequirementBlock, splitRequirementFenceContent, } from "../schema/index.js";
|
|
22
10
|
/** Matches a fenced dotrequirements block; group 1 is its inner content. */
|
|
23
11
|
const FENCE = /```dotrequirements\n([\s\S]*?)```/gm;
|
|
24
12
|
/**
|
|
@@ -103,7 +91,10 @@ export function requirementUnitMap(blockContent) {
|
|
|
103
91
|
const map = new Map();
|
|
104
92
|
for (const node of getAllRequirements([tree])) {
|
|
105
93
|
// A NUL (\u0000) separates the label from content so a label change reads as a change.
|
|
106
|
-
|
|
94
|
+
// The comparison id is the canonical root key plus the numeric position
|
|
95
|
+
// tail, so both sides agree on identity even when they spell the key
|
|
96
|
+
// differently (DIFF-3.3).
|
|
97
|
+
map.set(canonicalRequirementPath(node.id), `${node.label}\u0000${node.content}`);
|
|
107
98
|
}
|
|
108
99
|
return map;
|
|
109
100
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@popoverai/dotrequirements",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Requirements
|
|
3
|
+
"version": "0.30.0",
|
|
4
|
+
"description": "Requirements as testable, human-readable data — CLI and test harness for spec-driven development with AI coding agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"dotrequirements": "./dist/cli.js",
|
|
@@ -30,11 +30,17 @@
|
|
|
30
30
|
},
|
|
31
31
|
"keywords": [
|
|
32
32
|
"requirements",
|
|
33
|
+
"specifications",
|
|
34
|
+
"spec-driven-development",
|
|
33
35
|
"testing",
|
|
34
36
|
"test-coverage",
|
|
37
|
+
"acceptance-criteria",
|
|
35
38
|
"bdd",
|
|
36
39
|
"tdd",
|
|
37
|
-
"ai-assistant"
|
|
40
|
+
"ai-assistant",
|
|
41
|
+
"ai-agents",
|
|
42
|
+
"claude-code",
|
|
43
|
+
"mcp"
|
|
38
44
|
],
|
|
39
45
|
"author": "Will Raymer @Popover",
|
|
40
46
|
"license": "MIT",
|
|
@@ -49,9 +55,7 @@
|
|
|
49
55
|
"@babel/parser": "^7.28.5",
|
|
50
56
|
"@babel/traverse": "^7.28.5",
|
|
51
57
|
"@babel/types": "^7.28.5",
|
|
52
|
-
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
53
58
|
"@types/prompts": "^2.4.9",
|
|
54
|
-
"@workos-inc/node": "^7.77.0",
|
|
55
59
|
"chalk": "^5.6.2",
|
|
56
60
|
"commander": "^12.1.0",
|
|
57
61
|
"convex": "^1.42.3",
|
|
@@ -70,7 +74,7 @@
|
|
|
70
74
|
"@types/node": "^20",
|
|
71
75
|
"@types/uuid": "^11.0.0",
|
|
72
76
|
"typescript": "^5",
|
|
73
|
-
"vitest": "^4.1.
|
|
77
|
+
"vitest": "^4.1.10"
|
|
74
78
|
},
|
|
75
79
|
"files": [
|
|
76
80
|
"dist",
|