@popoverai/dotrequirements 0.34.0 → 0.34.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.
- package/README.md +2 -2
- package/dist/commands/acceptance-test.js +1 -1
- package/dist/commands/solution/brief.d.ts +5 -0
- package/dist/commands/solution/index.js +3 -2
- package/dist/commands/solution/run-file.d.ts +7 -6
- package/dist/commands/solution/run-file.js +9 -3
- package/dist/commands/solution/start.d.ts +6 -3
- package/dist/commands/solution/start.js +16 -5
- package/dist/commands/solution/status.js +12 -2
- package/dist/schema/browser.d.ts +1 -0
- package/dist/schema/browser.js +3 -0
- package/dist/schema/provenance.d.ts +27 -0
- package/dist/schema/provenance.js +66 -0
- package/dist/sync/execute.js +7 -8
- package/dist/sync/solution-documents.d.ts +15 -5
- package/dist/sync/solution-documents.js +46 -8
- package/dist/sync/types.d.ts +2 -0
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -251,13 +251,13 @@ Browser testing needs a Stagehand model and the API key for that model's provide
|
|
|
251
251
|
{
|
|
252
252
|
"defaultURL": "https://your-app.com",
|
|
253
253
|
"browserTest": {
|
|
254
|
-
"modelName": "gateway/anthropic/claude-haiku-
|
|
254
|
+
"modelName": "gateway/anthropic/claude-haiku-5.5",
|
|
255
255
|
"modelApiKey": "your-api-key"
|
|
256
256
|
}
|
|
257
257
|
}
|
|
258
258
|
```
|
|
259
259
|
|
|
260
|
-
Pick any Stagehand-supported model. Example values: `gateway/anthropic/claude-haiku-
|
|
260
|
+
Pick any Stagehand-supported model. Example values: `gateway/anthropic/claude-haiku-5.5` (Vercel AI Gateway key), `google/gemini-3-flash-preview` (Gemini key).
|
|
261
261
|
|
|
262
262
|
Optional settings: `vercelBypassSecret`, `browserbaseApiKey`, `browserbaseProjectId`. Setting both Browserbase fields switches runs from LOCAL (spawns Playwright on your machine) to BROWSERBASE (managed cloud browsers).
|
|
263
263
|
|
|
@@ -52,7 +52,7 @@ export async function acceptanceTestCommand(requirementKey, url, options) {
|
|
|
52
52
|
console.error("Error: no browser-test model configured in project-settings.json.");
|
|
53
53
|
console.log("Add a block like:\n" +
|
|
54
54
|
' "browserTest": {\n' +
|
|
55
|
-
' "modelName": "gateway/anthropic/claude-haiku-
|
|
55
|
+
' "modelName": "gateway/anthropic/claude-haiku-5.5",\n' +
|
|
56
56
|
' "modelApiKey": "..."\n' +
|
|
57
57
|
" }\n" +
|
|
58
58
|
"to .requirements/project-settings.json. See the docs for supported models.");
|
|
@@ -49,6 +49,11 @@ export type PanelOutcome = {
|
|
|
49
49
|
waitingFor?: string[];
|
|
50
50
|
/** Its drafter's work was set aside and its new draft isn't taken yet: a session at it owes nothing more (CHECKPOINT-6.3.2.3). */
|
|
51
51
|
setAside?: boolean;
|
|
52
|
+
/** The agents at it now, each drafting it or reviewing it; absent from a backend from before it said. */
|
|
53
|
+
atWork?: Array<{
|
|
54
|
+
agent: string;
|
|
55
|
+
part: "draft" | "review";
|
|
56
|
+
}>;
|
|
52
57
|
};
|
|
53
58
|
/** A checkpoint on the plan panel, as far as the CLI reads it. */
|
|
54
59
|
export type PanelCheckpoint = {
|
|
@@ -73,8 +73,9 @@ outcome at .requirements/solutions/<solution>/<outcome>.md. With a repository
|
|
|
73
73
|
connected, a file kept on the pull request is committed and submitted as
|
|
74
74
|
file:<path>, unsynced, since it reaches the project's requirements when the
|
|
75
75
|
pull request merges; one kept in the app is synced alone with
|
|
76
|
-
\`dotreq sync --repo-
|
|
77
|
-
frontmatter
|
|
76
|
+
\`dotreq sync <path> --repo-wins\`, which writes the document id into its
|
|
77
|
+
frontmatter and lets a revision replace the version before it, and submitted
|
|
78
|
+
as document:<id>. With none, submit file:<path>: it
|
|
78
79
|
is saved on the solution's branch, and the document id is written into its
|
|
79
80
|
frontmatter.
|
|
80
81
|
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Which solution and agent a verb is for (VERB-4). Inside a runner the run
|
|
3
3
|
* itself says — the runner writes a run file and names it in
|
|
4
|
-
* `DOTREQ_RUN_FILE`: `{ solutionId, agent, branch }` — so an agent
|
|
5
|
-
* ids. Outside one, the agent names them with `--solution` and
|
|
6
|
-
* which win wherever they are given. The outcome is the
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* `DOTREQ_RUN_FILE`: `{ solutionId, agent, branch, outcome }` — so an agent
|
|
5
|
+
* types no ids. Outside one, the agent names them with `--solution` and
|
|
6
|
+
* `--agent`, which win wherever they are given. The outcome is the one the
|
|
7
|
+
* run was handed, so a verb records on the outcome the agent is at even
|
|
8
|
+
* when it drafts one outcome and reviews another (VERB-3.4, 6.0);
|
|
9
|
+
* `--outcome` names another. Outside a run, the backend works it out.
|
|
9
10
|
*/
|
|
10
11
|
/** The options every solution verb an agent runs carries. */
|
|
11
12
|
export type WhereOptions = {
|
|
@@ -19,7 +20,7 @@ export type Where = {
|
|
|
19
20
|
solutionId: string;
|
|
20
21
|
/** The agent working: a plan's named agent, or one dedicated to an outcome ("Pitch/drafter"). */
|
|
21
22
|
agent: string;
|
|
22
|
-
/** The outcome
|
|
23
|
+
/** The outcome the agent named, or the one its run was handed; otherwise the backend's to work out. */
|
|
23
24
|
outcome?: string;
|
|
24
25
|
/**
|
|
25
26
|
* The solution's branch, which the runner has checked out (SOLBRANCH-1):
|
|
@@ -17,11 +17,12 @@ function fromRun() {
|
|
|
17
17
|
}
|
|
18
18
|
if (!parsed || typeof parsed !== "object")
|
|
19
19
|
return {};
|
|
20
|
-
const { solutionId, agent, branch } = parsed;
|
|
20
|
+
const { solutionId, agent, branch, outcome } = parsed;
|
|
21
21
|
return {
|
|
22
22
|
solutionId: typeof solutionId === "string" ? solutionId : undefined,
|
|
23
23
|
agent: typeof agent === "string" && agent ? agent : undefined,
|
|
24
24
|
branch: typeof branch === "string" && branch ? branch : undefined,
|
|
25
|
+
outcome: typeof outcome === "string" && outcome ? outcome : undefined,
|
|
25
26
|
};
|
|
26
27
|
}
|
|
27
28
|
/** The outcome named, by `--outcome` or its old name `--step`; blank is none. */
|
|
@@ -31,10 +32,15 @@ function outcomeNamed(options) {
|
|
|
31
32
|
/** The run's own, with anything the agent named winning over it. */
|
|
32
33
|
export function whereOf(options) {
|
|
33
34
|
const run = fromRun();
|
|
35
|
+
const agent = options.agent?.trim() || run.agent;
|
|
36
|
+
// The run's outcome is its own agent's on its own solution: naming
|
|
37
|
+
// another is working outside the run.
|
|
38
|
+
const inTheRun = (options.solution === undefined || options.solution === run.solutionId) &&
|
|
39
|
+
agent === run.agent;
|
|
34
40
|
return {
|
|
35
41
|
solutionId: options.solution ?? run.solutionId,
|
|
36
|
-
agent
|
|
37
|
-
outcome: outcomeNamed(options),
|
|
42
|
+
agent,
|
|
43
|
+
outcome: outcomeNamed(options) ?? (inTheRun ? run.outcome : undefined),
|
|
38
44
|
// The run's branch is the run's solution's: naming another solution
|
|
39
45
|
// is working outside the run.
|
|
40
46
|
branch: options.solution === undefined || options.solution === run.solutionId
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `dotreq solution start <solution-id> <agent>`: begin an agent's work by
|
|
3
|
-
* hand. A stalled agent is started again
|
|
4
|
-
*
|
|
5
|
-
* plan's order
|
|
3
|
+
* hand. A stalled agent is started again. With the project's agent
|
|
4
|
+
* connected, it is sent for and its runner takes the first part it has
|
|
5
|
+
* due, in the plan's order, as after Start again in the app; `--outcome`
|
|
6
|
+
* naming any other is refused. With none connected, it takes the part of
|
|
7
|
+
* an outcome's work it has due itself: the one `--outcome` names, or its
|
|
8
|
+
* first in the plan's order (STEP-REVIEW-24.10, 24.11).
|
|
6
9
|
*/
|
|
7
10
|
export declare function startCommand(solutionId: string, agent: string, options: {
|
|
8
11
|
outcome?: string;
|
|
@@ -3,18 +3,29 @@ import { solutionClient } from "./client.js";
|
|
|
3
3
|
import { onOutcome } from "./run-file.js";
|
|
4
4
|
/**
|
|
5
5
|
* `dotreq solution start <solution-id> <agent>`: begin an agent's work by
|
|
6
|
-
* hand. A stalled agent is started again
|
|
7
|
-
*
|
|
8
|
-
* plan's order
|
|
6
|
+
* hand. A stalled agent is started again. With the project's agent
|
|
7
|
+
* connected, it is sent for and its runner takes the first part it has
|
|
8
|
+
* due, in the plan's order, as after Start again in the app; `--outcome`
|
|
9
|
+
* naming any other is refused. With none connected, it takes the part of
|
|
10
|
+
* an outcome's work it has due itself: the one `--outcome` names, or its
|
|
11
|
+
* first in the plan's order (STEP-REVIEW-24.10, 24.11).
|
|
9
12
|
*/
|
|
10
13
|
export async function startCommand(solutionId, agent, options) {
|
|
11
14
|
const outcome = (options.outcome ?? options.step)?.trim() || undefined;
|
|
12
15
|
const client = solutionClient();
|
|
13
|
-
await client.mutation(api.solutions.mutations.start, {
|
|
16
|
+
const started = await client.mutation(api.solutions.mutations.start, {
|
|
14
17
|
solutionId,
|
|
15
18
|
agent,
|
|
16
19
|
...(outcome ? { outcome } : {}),
|
|
17
20
|
});
|
|
18
|
-
|
|
21
|
+
if (!started) {
|
|
22
|
+
// A backend from before it said what it did.
|
|
23
|
+
console.log(`Started ${agent}${onOutcome(outcome)}.`);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const work = `${started.part === "review" ? "review" : "draft"} "${started.outcome}"`;
|
|
27
|
+
console.log(started.sentFor
|
|
28
|
+
? `Sent for ${agent}: its runner will ${work}.`
|
|
29
|
+
: `Started ${agent}: it can ${work}.`);
|
|
19
30
|
}
|
|
20
31
|
//# sourceMappingURL=start.js.map
|
|
@@ -23,6 +23,11 @@ export function agentAt(panel, agent, named) {
|
|
|
23
23
|
if (!drafting && !reviewing) {
|
|
24
24
|
throw new Error(`${agent} neither drafts nor reviews "${named}".`);
|
|
25
25
|
}
|
|
26
|
+
// At it now: the part it is at, as for an agent that both drafts and
|
|
27
|
+
// reviews the outcome, whatever word the outcome stands in.
|
|
28
|
+
const at = outcome.atWork?.find((each) => each.agent === agent);
|
|
29
|
+
if (at)
|
|
30
|
+
return { outcome, part: at.part };
|
|
26
31
|
return {
|
|
27
32
|
outcome,
|
|
28
33
|
part: reviewing && (!drafting || outcome.status === "reviewing")
|
|
@@ -129,10 +134,15 @@ export async function statusCommand(options) {
|
|
|
129
134
|
// CHECKPOINT-6.3.2.3: a drafter whose work was set aside owes nothing
|
|
130
135
|
// more there; the new draft is another session's.
|
|
131
136
|
const setAside = part === "draft" && outcome.setAside === true;
|
|
137
|
+
// STEP-REVIEW-9.8: an agent still at its part owes it, whatever word the
|
|
138
|
+
// outcome stands in because of its other agents, as stalled when the
|
|
139
|
+
// other reviewer stalled. Asking, stalling and recording all take the
|
|
140
|
+
// agent off it.
|
|
141
|
+
const atIt = outcome.atWork?.some((each) => each.agent === where.agent && each.part === part) === true;
|
|
132
142
|
const owes = part === "draft"
|
|
133
143
|
? !setAside &&
|
|
134
|
-
(outcome.status === "drafting" || outcome.status === "ready")
|
|
135
|
-
: outcome.status === "reviewing" &&
|
|
144
|
+
(atIt || outcome.status === "drafting" || outcome.status === "ready")
|
|
145
|
+
: (atIt || outcome.status === "reviewing") &&
|
|
136
146
|
(await reviewerOwes(client, where.solutionId, outcome.name, where.agent));
|
|
137
147
|
if (options.json) {
|
|
138
148
|
process.stdout.write(`${JSON.stringify({ agent: where.agent, outcome: outcome.name, part, status: outcome.status, reason: outcome.reason ?? null, reviews: part === "review", ...(setAside ? { setAside } : {}), owes })}\n`);
|
package/dist/schema/browser.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export { buildRequirementMarkdown, buildRequirementsMarkdown, DEFAULT_DELIMITER,
|
|
|
7
7
|
export type { ConvexRequirement } from "./conversions.js";
|
|
8
8
|
export { buildMetadata, constructKey, convexToRequirements, extractRequirementKeys, groupByRoot, parseKey, requirementsToConvex, } from "./conversions.js";
|
|
9
9
|
export { DELIMITER_PATTERN, type ExtractedRequirementBlock, extractRequirementBlocks, findRequirementById, flattenRequirementTree, getAllRequirements, parseCriterionLine, parseRequirementBlock, parseRequirementBlocksFromMarkdown, parseRootLine, ROOT_LABEL_MARKER, splitRequirementFenceContent, stripFrontmatterBlock, } from "./parser-core.js";
|
|
10
|
+
export { carriesBranchVersion } from "./provenance.js";
|
|
10
11
|
export type { Metadata, ParsedCriterion, RequirementKey, RequirementNode, RequirementPrefix, RequirementsFile, } from "./schemas.js";
|
|
11
12
|
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
13
|
export { composeMarkdownWithTitle, effectiveTitle, type SplitMarkdown, splitLeadingH1, } from "./title-markdown.js";
|
package/dist/schema/browser.js
CHANGED
|
@@ -14,6 +14,9 @@ export { DELIMITER_PATTERN, extractRequirementBlocks, findRequirementById, flatt
|
|
|
14
14
|
ROOT_LABEL_MARKER, splitRequirementFenceContent,
|
|
15
15
|
// A requirements file read from a repository in the browser (SOLDOC-12).
|
|
16
16
|
stripFrontmatterBlock, } from "./parser-core.js";
|
|
17
|
+
// Whether a publish that says it lands a branch version matches it
|
|
18
|
+
// (BRANCH-7.11), checked by the web app's merge and by the backend alike
|
|
19
|
+
export { carriesBranchVersion } from "./provenance.js";
|
|
17
20
|
// Schemas and types (uses zod - browser-safe)
|
|
18
21
|
export { buildRequirementKey, canonicalRequirementKey,
|
|
19
22
|
// Zod schemas
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a publish that says it lands a branch version matches that version
|
|
3
|
+
* (BRANCH-7.11): its title, its default prefix, and each of its requirements,
|
|
4
|
+
* key and wording, in the same tree and order. Pure and browser-safe, so the web
|
|
5
|
+
* app's merge checks every document before landing any, and the backend
|
|
6
|
+
* checks each publish, by the same rule.
|
|
7
|
+
*
|
|
8
|
+
* The merge publishes each version as the app's editor stores it, so the
|
|
9
|
+
* comparison is of what the editor keeps, not of the text:
|
|
10
|
+
* - a criterion's position is its place in the tree, since the editor
|
|
11
|
+
* numbers criteria from 0 in order, and a label is compared without case,
|
|
12
|
+
* since it writes a Gherkin keyword as `Given` however it was written;
|
|
13
|
+
* - the title is the leading heading however it is written, `#` or
|
|
14
|
+
* underlined, falling back to the version's own title as the merge does;
|
|
15
|
+
* - the writing between requirements isn't compared at all (7.11.0): the
|
|
16
|
+
* editor lays lists and tables out its own way and drops what it doesn't
|
|
17
|
+
* keep, such as an HTML comment (BRANCH-2.2).
|
|
18
|
+
*/
|
|
19
|
+
export declare function carriesBranchVersion(published: {
|
|
20
|
+
markdownContent: string;
|
|
21
|
+
defaultPrefix?: string;
|
|
22
|
+
}, version: {
|
|
23
|
+
markdownContent: string;
|
|
24
|
+
title?: string;
|
|
25
|
+
defaultPrefix?: string;
|
|
26
|
+
}): boolean;
|
|
27
|
+
//# sourceMappingURL=provenance.d.ts.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { parseRequirementBlocksFromMarkdown } from "./parser-core.js";
|
|
2
|
+
import { splitLeadingH1 } from "./title-markdown.js";
|
|
3
|
+
/**
|
|
4
|
+
* Whether a publish that says it lands a branch version matches that version
|
|
5
|
+
* (BRANCH-7.11): its title, its default prefix, and each of its requirements,
|
|
6
|
+
* key and wording, in the same tree and order. Pure and browser-safe, so the web
|
|
7
|
+
* app's merge checks every document before landing any, and the backend
|
|
8
|
+
* checks each publish, by the same rule.
|
|
9
|
+
*
|
|
10
|
+
* The merge publishes each version as the app's editor stores it, so the
|
|
11
|
+
* comparison is of what the editor keeps, not of the text:
|
|
12
|
+
* - a criterion's position is its place in the tree, since the editor
|
|
13
|
+
* numbers criteria from 0 in order, and a label is compared without case,
|
|
14
|
+
* since it writes a Gherkin keyword as `Given` however it was written;
|
|
15
|
+
* - the title is the leading heading however it is written, `#` or
|
|
16
|
+
* underlined, falling back to the version's own title as the merge does;
|
|
17
|
+
* - the writing between requirements isn't compared at all (7.11.0): the
|
|
18
|
+
* editor lays lists and tables out its own way and drops what it doesn't
|
|
19
|
+
* keep, such as an HTML comment (BRANCH-2.2).
|
|
20
|
+
*/
|
|
21
|
+
export function carriesBranchVersion(published, version) {
|
|
22
|
+
const theirs = requirementsOf(published.markdownContent);
|
|
23
|
+
const ours = requirementsOf(version.markdownContent);
|
|
24
|
+
return (theirs !== null &&
|
|
25
|
+
ours !== null &&
|
|
26
|
+
theirs.length === ours.length &&
|
|
27
|
+
theirs.every((each, i) => each === ours[i]) &&
|
|
28
|
+
(titleOf(published.markdownContent) ?? "") ===
|
|
29
|
+
(titleOf(version.markdownContent) ?? version.title ?? "") &&
|
|
30
|
+
(published.defaultPrefix ?? "") === (version.defaultPrefix ?? ""));
|
|
31
|
+
}
|
|
32
|
+
/** Each requirement as the editor keeps it, in order; null when a block doesn't parse. */
|
|
33
|
+
function requirementsOf(markdown) {
|
|
34
|
+
let trees;
|
|
35
|
+
try {
|
|
36
|
+
trees = parseRequirementBlocksFromMarkdown(markdown);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const out = [];
|
|
42
|
+
const walk = (node, path) => {
|
|
43
|
+
out.push([path, node.label.trim().toLowerCase(), squash(node.content)].join("\u0000"));
|
|
44
|
+
node.children.forEach((child, index) => {
|
|
45
|
+
walk(child, `${path}.${index}`);
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
for (const tree of trees)
|
|
49
|
+
walk(tree, tree.id.split(".")[0]);
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
function squash(text) {
|
|
53
|
+
return text.replace(/\s+/g, " ").trim();
|
|
54
|
+
}
|
|
55
|
+
/** The leading heading's text, `#` or underlined with `=`; undefined when there is none. */
|
|
56
|
+
function titleOf(markdown) {
|
|
57
|
+
const atx = splitLeadingH1(markdown).title;
|
|
58
|
+
if (atx != null)
|
|
59
|
+
return squash(atx);
|
|
60
|
+
const lines = markdown.replace(/^\s*\n/, "").split("\n");
|
|
61
|
+
if (lines.length >= 2 && lines[0].trim() && /^=+\s*$/.test(lines[1])) {
|
|
62
|
+
return squash(lines[0]);
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=provenance.js.map
|
package/dist/sync/execute.js
CHANGED
|
@@ -435,7 +435,8 @@ function localWrite(doc, cloudById, usedPaths, requirementsDir, outcome, mode) {
|
|
|
435
435
|
/**
|
|
436
436
|
* SOLDOC-6: where a pulled document lands. A file it is already linked to
|
|
437
437
|
* keeps its path (SYNC-DISCOVERY-2.0); a document that belongs to a solution
|
|
438
|
-
* lands in that solution's directory, named for its outcome
|
|
438
|
+
* lands in that solution's directory, named for its outcome (SOLDOC-19 when
|
|
439
|
+
* the outcome holds more than one); a document
|
|
439
440
|
* that is not a requirements document and belongs to no solution has no
|
|
440
441
|
* place, and the pull says so rather than guessing.
|
|
441
442
|
*/
|
|
@@ -445,13 +446,11 @@ function placeCloudDocument(cloudDoc, existingPath, usedPaths, requirementsDir)
|
|
|
445
446
|
const isRequirementsDoc = cloudDoc.isRequirementsDoc !== false;
|
|
446
447
|
if (cloudDoc.solution) {
|
|
447
448
|
// SOLDOC-9: the solution's own directory name; a backend from before it
|
|
448
|
-
// was sent leaves it out, and the title's slug stands in.
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
}
|
|
454
|
-
return candidate;
|
|
449
|
+
// was sent leaves it out, and the title's slug stands in. SOLDOC-19: the
|
|
450
|
+
// file name the backend gives it, or the next free one when another file
|
|
451
|
+
// is there.
|
|
452
|
+
return solutionDocumentPath(requirementsDir, cloudDoc.solution.directory ??
|
|
453
|
+
sanitizeFileName(cloudDoc.solution.title ?? cloudDoc.solution.solutionId), cloudDoc.solution.outcome, isRequirementsDoc, cloudDoc.solution.fileName, (candidate) => usedPaths.has(candidate) || fs.existsSync(candidate));
|
|
455
454
|
}
|
|
456
455
|
if (!isRequirementsDoc) {
|
|
457
456
|
throw new Error(`"${cloudDoc.title}" is not a requirements document and belongs to no solution, so the pull has nowhere to put it.`);
|
|
@@ -55,10 +55,20 @@ export declare function resolveBelonging(filePath: string, frontmatter: {
|
|
|
55
55
|
outcome?: string;
|
|
56
56
|
}, claim: PathClaim | undefined, solutions: SolutionForSync[] | undefined): Resolved;
|
|
57
57
|
/**
|
|
58
|
-
* SOLDOC-
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
58
|
+
* SOLDOC-4, SOLDOC-19: the outcome a file's stem names, of those given: the
|
|
59
|
+
* one whose slug the stem is, or else the one whose slug it is followed by a
|
|
60
|
+
* suffix from -2 up, as an outcome's later documents are named. So `spec-2`
|
|
61
|
+
* is Spec 2's when there is a Spec 2, and the Spec's otherwise.
|
|
62
62
|
*/
|
|
63
|
-
export declare function
|
|
63
|
+
export declare function outcomeNamedBy(stem: string, outcomes: readonly string[]): string | undefined;
|
|
64
|
+
/**
|
|
65
|
+
* SOLDOC-6, SOLDOC-9, SOLDOC-19: where a pulled document that belongs to a
|
|
66
|
+
* solution lands when it has no file yet: in the solution's own directory,
|
|
67
|
+
* at the file name the backend gives it, which counts the outcome's other
|
|
68
|
+
* documents. A backend from before it was sent leaves it out, and the
|
|
69
|
+
* outcome's plain name stands in: its slug, with the `.requirements.md` name
|
|
70
|
+
* when it is a requirements document. When that file is taken, the document
|
|
71
|
+
* takes the outcome's next free name from -2 up.
|
|
72
|
+
*/
|
|
73
|
+
export declare function solutionDocumentPath(requirementsDir: string, solutionDirectory: string, outcome: string, isRequirementsDoc: boolean, fileName?: string | null, taken?: (candidate: string) => boolean): string;
|
|
64
74
|
//# sourceMappingURL=solution-documents.d.ts.map
|
|
@@ -60,7 +60,12 @@ export function resolveBelonging(filePath, frontmatter, claim, solutions) {
|
|
|
60
60
|
warning: `${name}: its path names a different solution than its frontmatter ("${owner.title}"); the frontmatter's belonging is kept.`,
|
|
61
61
|
};
|
|
62
62
|
}
|
|
63
|
-
|
|
63
|
+
// Read against the solution's other outcomes too: `spec-2` names Spec 2,
|
|
64
|
+
// not the Spec, when the plan has both.
|
|
65
|
+
if (outcomeNamedBy(claim.outcomeSlug, [
|
|
66
|
+
...(owner?.outcomes ?? []),
|
|
67
|
+
declared.outcome,
|
|
68
|
+
]) !== declared.outcome) {
|
|
64
69
|
return {
|
|
65
70
|
belonging: declared,
|
|
66
71
|
warning: `${name}: its filename names a different outcome than its frontmatter ("${declared.outcome}"); the frontmatter's belonging is kept.`,
|
|
@@ -78,20 +83,53 @@ export function resolveBelonging(filePath, frontmatter, claim, solutions) {
|
|
|
78
83
|
throw new Error(`${name}: its directory "${claim.solutionSlug}" matches more than one solution (${matches.map((s) => `"${s.title}"`).join(", ")}), so it can't be placed.`);
|
|
79
84
|
}
|
|
80
85
|
const [solution] = matches;
|
|
81
|
-
const outcome =
|
|
86
|
+
const outcome = outcomeNamedBy(claim.outcomeSlug, solution.outcomes);
|
|
82
87
|
if (!outcome) {
|
|
83
88
|
throw new Error(`${name}: "${claim.outcomeSlug}" matches no outcome of "${solution.title}". Its plan produces: ${solution.outcomes.map((o) => `${o} (${sanitizeFileName(o)}.md)`).join(", ")}.`);
|
|
84
89
|
}
|
|
85
90
|
return { belonging: { solutionId: solution.solutionId, outcome } };
|
|
86
91
|
}
|
|
87
92
|
/**
|
|
88
|
-
* SOLDOC-
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
93
|
+
* SOLDOC-4, SOLDOC-19: the outcome a file's stem names, of those given: the
|
|
94
|
+
* one whose slug the stem is, or else the one whose slug it is followed by a
|
|
95
|
+
* suffix from -2 up, as an outcome's later documents are named. So `spec-2`
|
|
96
|
+
* is Spec 2's when there is a Spec 2, and the Spec's otherwise.
|
|
92
97
|
*/
|
|
93
|
-
export function
|
|
98
|
+
export function outcomeNamedBy(stem, outcomes) {
|
|
99
|
+
const plain = outcomes.find((o) => sanitizeFileName(o) === stem);
|
|
100
|
+
if (plain !== undefined)
|
|
101
|
+
return plain;
|
|
102
|
+
const later = /^(.+)-([1-9]\d*)$/.exec(stem);
|
|
103
|
+
if (!later || Number(later[2]) < 2)
|
|
104
|
+
return undefined;
|
|
105
|
+
return outcomes.find((o) => sanitizeFileName(o) === later[1]);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* SOLDOC-6, SOLDOC-9, SOLDOC-19: where a pulled document that belongs to a
|
|
109
|
+
* solution lands when it has no file yet: in the solution's own directory,
|
|
110
|
+
* at the file name the backend gives it, which counts the outcome's other
|
|
111
|
+
* documents. A backend from before it was sent leaves it out, and the
|
|
112
|
+
* outcome's plain name stands in: its slug, with the `.requirements.md` name
|
|
113
|
+
* when it is a requirements document. When that file is taken, the document
|
|
114
|
+
* takes the outcome's next free name from -2 up.
|
|
115
|
+
*/
|
|
116
|
+
export function solutionDocumentPath(requirementsDir, solutionDirectory, outcome, isRequirementsDoc, fileName, taken = () => false) {
|
|
94
117
|
const suffix = isRequirementsDoc ? ".requirements.md" : ".md";
|
|
95
|
-
|
|
118
|
+
const directory = path.join(requirementsDir, SOLUTIONS_DIR, solutionDirectory || "solution");
|
|
119
|
+
const slug = sanitizeFileName(outcome) || "outcome";
|
|
120
|
+
// Only a plain file name of the document's kind, never a path.
|
|
121
|
+
const given = fileName &&
|
|
122
|
+
fileName === path.basename(fileName) &&
|
|
123
|
+
!fileName.startsWith(".") &&
|
|
124
|
+
stemOf(fileName) !== "" &&
|
|
125
|
+
fileName.endsWith(suffix) &&
|
|
126
|
+
(isRequirementsDoc || !fileName.endsWith(".requirements.md"))
|
|
127
|
+
? fileName
|
|
128
|
+
: `${slug}${suffix}`;
|
|
129
|
+
let candidate = path.join(directory, given);
|
|
130
|
+
for (let n = 2; taken(candidate); n++) {
|
|
131
|
+
candidate = path.join(directory, `${slug}-${n}${suffix}`);
|
|
132
|
+
}
|
|
133
|
+
return candidate;
|
|
96
134
|
}
|
|
97
135
|
//# sourceMappingURL=solution-documents.js.map
|
package/dist/sync/types.d.ts
CHANGED
|
@@ -56,6 +56,8 @@ export interface CloudDocument {
|
|
|
56
56
|
title: string | null;
|
|
57
57
|
/** SOLDOC-9: the solution's directory name; absent from an older backend. */
|
|
58
58
|
directory?: string | null;
|
|
59
|
+
/** SOLDOC-19: the document's file name in that directory; absent from an older backend. */
|
|
60
|
+
fileName?: string | null;
|
|
59
61
|
};
|
|
60
62
|
/** SOLDOC-2/3: absent means a requirements document. */
|
|
61
63
|
isRequirementsDoc?: boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@popoverai/dotrequirements",
|
|
3
|
-
"version": "0.34.
|
|
3
|
+
"version": "0.34.2",
|
|
4
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": {
|
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
"test": "vitest",
|
|
27
27
|
"test:run": "vitest run",
|
|
28
28
|
"test:coverage": "vitest run --coverage",
|
|
29
|
-
"publish:patch": "echo 'ERROR: publish:patch is decommissioned. CLI npm publishes now happen via the cli-publish GitHub Action when a
|
|
30
|
-
"publish:minor": "echo 'ERROR: publish:minor is decommissioned. CLI npm publishes now happen via the cli-publish GitHub Action when a
|
|
31
|
-
"publish:major": "echo 'ERROR: publish:major is decommissioned. CLI npm publishes now happen via the cli-publish GitHub Action when a
|
|
29
|
+
"publish:patch": "echo 'ERROR: publish:patch is decommissioned. CLI npm publishes now happen via the cli-publish GitHub Action when a PR that bumps the CLI version merges to main. See docs/working/cli-publish-workflow.md for details.' && exit 1",
|
|
30
|
+
"publish:minor": "echo 'ERROR: publish:minor is decommissioned. CLI npm publishes now happen via the cli-publish GitHub Action when a PR that bumps the CLI version merges to main. See docs/working/cli-publish-workflow.md for details.' && exit 1",
|
|
31
|
+
"publish:major": "echo 'ERROR: publish:major is decommissioned. CLI npm publishes now happen via the cli-publish GitHub Action when a PR that bumps the CLI version merges to main. See docs/working/cli-publish-workflow.md for details.' && exit 1"
|
|
32
32
|
},
|
|
33
33
|
"keywords": [
|
|
34
34
|
"requirements",
|