@popoverai/dotrequirements 0.26.0 → 0.26.1
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/area-name.d.ts +13 -0
- package/dist/codebase-to-spec/area-name.js +18 -0
- package/dist/codebase-to-spec/cache.d.ts +31 -0
- package/dist/codebase-to-spec/cache.js +29 -1
- package/dist/codebase-to-spec/compose.d.ts +7 -4
- package/dist/codebase-to-spec/compose.js +10 -21
- package/dist/codebase-to-spec/dispatch.js +1 -1
- package/dist/codebase-to-spec/present.js +7 -2
- package/dist/codebase-to-spec/prompts/planner-initial.d.ts +3 -2
- package/dist/codebase-to-spec/prompts/planner-initial.js +3 -2
- package/dist/codebase-to-spec/renumber.d.ts +52 -0
- package/dist/codebase-to-spec/renumber.js +105 -0
- package/dist/codebase-to-spec/schemas.d.ts +35 -240
- package/dist/codebase-to-spec/schemas.js +5 -173
- package/dist/commands/codebase-to-spec/dispatch-editor.js +1 -1
- package/dist/commands/codebase-to-spec/dispatch-spec.js +1 -1
- package/dist/commands/codebase-to-spec/index.js +7 -103
- package/dist/commands/codebase-to-spec/pack.js +8 -1
- package/dist/commands/codebase-to-spec/present-orchestrator.d.ts +3 -1
- package/dist/commands/codebase-to-spec/present-orchestrator.js +11 -4
- package/dist/templates/skills/codebase-to-spec/SKILL.md +10 -4
- package/package.json +1 -1
- package/dist/codebase-to-spec/edit-loop.d.ts +0 -54
- package/dist/codebase-to-spec/edit-loop.js +0 -195
- package/dist/codebase-to-spec/editor.d.ts +0 -54
- package/dist/codebase-to-spec/editor.js +0 -74
- package/dist/codebase-to-spec/fan-out.d.ts +0 -63
- package/dist/codebase-to-spec/fan-out.js +0 -215
- package/dist/codebase-to-spec/outline-review-loop.d.ts +0 -51
- package/dist/codebase-to-spec/outline-review-loop.js +0 -187
- package/dist/codebase-to-spec/planner.d.ts +0 -41
- package/dist/codebase-to-spec/planner.js +0 -76
- package/dist/codebase-to-spec/prompts/outline-reviewer.d.ts +0 -12
- package/dist/codebase-to-spec/prompts/outline-reviewer.js +0 -89
- package/dist/codebase-to-spec/slice.d.ts +0 -49
- package/dist/codebase-to-spec/slice.js +0 -111
- package/dist/codebase-to-spec/specifier.d.ts +0 -60
- package/dist/codebase-to-spec/specifier.js +0 -85
- package/dist/codebase-to-spec/summary.d.ts +0 -51
- package/dist/codebase-to-spec/summary.js +0 -183
- package/dist/commands/codebase-to-spec/compose.d.ts +0 -14
- package/dist/commands/codebase-to-spec/compose.js +0 -57
- package/dist/commands/codebase-to-spec/edit-loop.d.ts +0 -16
- package/dist/commands/codebase-to-spec/edit-loop.js +0 -83
- package/dist/commands/codebase-to-spec/fan-out.d.ts +0 -19
- package/dist/commands/codebase-to-spec/fan-out.js +0 -77
- package/dist/commands/codebase-to-spec/plan-loop.d.ts +0 -26
- package/dist/commands/codebase-to-spec/plan-loop.js +0 -105
- package/dist/commands/codebase-to-spec/present.d.ts +0 -26
- package/dist/commands/codebase-to-spec/present.js +0 -97
- package/dist/commands/codebase-to-spec/run.d.ts +0 -20
- package/dist/commands/codebase-to-spec/run.js +0 -86
- package/dist/commands/codebase-to-spec/specify-area.d.ts +0 -18
- package/dist/commands/codebase-to-spec/specify-area.js +0 -82
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Area-name slugging shared across the codebase-to-spec stages.
|
|
3
|
+
*
|
|
4
|
+
* The pack/specify/compose/present stages and the worker dispatch surface all
|
|
5
|
+
* derive cache and output file names from an area's display name; they agree on
|
|
6
|
+
* the slug by routing through this single helper.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Convert an area name to a stable filesystem-safe identifier.
|
|
10
|
+
* E.g., "Reading & Editing Files" → "reading-editing-files"
|
|
11
|
+
*/
|
|
12
|
+
export declare function sanitizeAreaName(name: string): string;
|
|
13
|
+
//# sourceMappingURL=area-name.d.ts.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Area-name slugging shared across the codebase-to-spec stages.
|
|
3
|
+
*
|
|
4
|
+
* The pack/specify/compose/present stages and the worker dispatch surface all
|
|
5
|
+
* derive cache and output file names from an area's display name; they agree on
|
|
6
|
+
* the slug by routing through this single helper.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Convert an area name to a stable filesystem-safe identifier.
|
|
10
|
+
* E.g., "Reading & Editing Files" → "reading-editing-files"
|
|
11
|
+
*/
|
|
12
|
+
export function sanitizeAreaName(name) {
|
|
13
|
+
return (name
|
|
14
|
+
.replace(/[^A-Za-z0-9]+/g, "-")
|
|
15
|
+
.replace(/^-+|-+$/g, "")
|
|
16
|
+
.toLowerCase() || "area");
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=area-name.js.map
|
|
@@ -44,8 +44,39 @@ export interface CachePaths {
|
|
|
44
44
|
* lifecycle state inline. Distinct from the legacy outline-N.json files.
|
|
45
45
|
*/
|
|
46
46
|
outline: string;
|
|
47
|
+
/**
|
|
48
|
+
* Persisted run-config marker (e.g., the isolation decision). Written once
|
|
49
|
+
* at pack time and read by later stages, so a choice like isolation is
|
|
50
|
+
* honored end-to-end without the caller resupplying it. See CTS-PRESENT-5.2.
|
|
51
|
+
*/
|
|
52
|
+
runConfig: string;
|
|
47
53
|
}
|
|
48
54
|
export declare function cachePaths(projectRoot: string): CachePaths;
|
|
55
|
+
/**
|
|
56
|
+
* Per-run configuration persisted in the cache so a decision made at one stage
|
|
57
|
+
* is honored by later stages. The isolation decision (CTS-PRESENT-5) lives here:
|
|
58
|
+
* pack records it once, and the present stage reads it rather than relying on
|
|
59
|
+
* the caller — or the orchestrating agent's memory — to carry it across the run.
|
|
60
|
+
*/
|
|
61
|
+
export interface RunConfig {
|
|
62
|
+
/**
|
|
63
|
+
* Whether this run is isolated from an existing `.requirements/` directory:
|
|
64
|
+
* the pack excluded it, and the present stage writes under `.requirements/cts/`.
|
|
65
|
+
*/
|
|
66
|
+
isolated: boolean;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Read the persisted run-config. Returns a non-isolated default when the marker
|
|
70
|
+
* is absent (an older cache, or a run that never wrote one) or unreadable, so a
|
|
71
|
+
* missing marker can never silently isolate output.
|
|
72
|
+
*/
|
|
73
|
+
export declare function readRunConfig(projectRoot: string): RunConfig;
|
|
74
|
+
/**
|
|
75
|
+
* Persist the run-config. Pack writes this on every invocation (with the current
|
|
76
|
+
* isolation value) so a re-pack without `--ignore-requirements` clears a stale
|
|
77
|
+
* isolated marker rather than leaving the prior run's choice in place.
|
|
78
|
+
*/
|
|
79
|
+
export declare function writeRunConfig(projectRoot: string, config: RunConfig): void;
|
|
49
80
|
export declare function ensureCacheDir(projectRoot: string): CachePaths;
|
|
50
81
|
/**
|
|
51
82
|
* Remove all cached artifacts. Used by `--fresh`.
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* - CTS-PACK-1: pack stage paths (overview.txt, source.txt)
|
|
11
11
|
* - CTS-RESUME-1..4: per-stage cache detection
|
|
12
12
|
*/
|
|
13
|
-
import { existsSync, mkdirSync, rmSync } from "node:fs";
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync, } from "node:fs";
|
|
14
14
|
import { join } from "node:path";
|
|
15
15
|
export const CACHE_DIR_NAME = ".dotrequirements-cache";
|
|
16
16
|
export function cachePaths(projectRoot) {
|
|
@@ -30,8 +30,36 @@ export function cachePaths(projectRoot) {
|
|
|
30
30
|
specFinal: join(root, "spec-final.md"),
|
|
31
31
|
pipelineSummary: join(root, "pipeline-summary.json"),
|
|
32
32
|
outline: join(root, "outline.yaml"),
|
|
33
|
+
runConfig: join(root, "run-config.json"),
|
|
33
34
|
};
|
|
34
35
|
}
|
|
36
|
+
const DEFAULT_RUN_CONFIG = { isolated: false };
|
|
37
|
+
/**
|
|
38
|
+
* Read the persisted run-config. Returns a non-isolated default when the marker
|
|
39
|
+
* is absent (an older cache, or a run that never wrote one) or unreadable, so a
|
|
40
|
+
* missing marker can never silently isolate output.
|
|
41
|
+
*/
|
|
42
|
+
export function readRunConfig(projectRoot) {
|
|
43
|
+
const path = cachePaths(projectRoot).runConfig;
|
|
44
|
+
if (!existsSync(path))
|
|
45
|
+
return { ...DEFAULT_RUN_CONFIG };
|
|
46
|
+
try {
|
|
47
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
48
|
+
return { isolated: parsed?.isolated === true };
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return { ...DEFAULT_RUN_CONFIG };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Persist the run-config. Pack writes this on every invocation (with the current
|
|
56
|
+
* isolation value) so a re-pack without `--ignore-requirements` clears a stale
|
|
57
|
+
* isolated marker rather than leaving the prior run's choice in place.
|
|
58
|
+
*/
|
|
59
|
+
export function writeRunConfig(projectRoot, config) {
|
|
60
|
+
const paths = ensureCacheDir(projectRoot);
|
|
61
|
+
writeFileSync(paths.runConfig, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
|
|
62
|
+
}
|
|
35
63
|
export function ensureCacheDir(projectRoot) {
|
|
36
64
|
const paths = cachePaths(projectRoot);
|
|
37
65
|
if (!existsSync(paths.root)) {
|
|
@@ -5,13 +5,15 @@
|
|
|
5
5
|
* title, a summary paragraph from the outline, and one H2 section per area.
|
|
6
6
|
* Each area section contains the partial's content.
|
|
7
7
|
*
|
|
8
|
-
* After assembly,
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* After assembly, a deterministic renumbering pass rewrites each prefix's
|
|
9
|
+
* requirement IDs into a clean 1..N sequence (sweeping the ID-sequence scars
|
|
10
|
+
* specifier/edit rounds leave behind), then schema validation runs over the
|
|
11
|
+
* document. Errors validation can't resolve surface to the caller.
|
|
11
12
|
*
|
|
12
13
|
* Requirements covered:
|
|
13
14
|
* - CTS-COMPOSE-1: Compose produces a single Markdown document from partials
|
|
14
15
|
* - CTS-COMPOSE-2: Compose validates the resulting document and corrects mechanical errors
|
|
16
|
+
* - CTS-COMPOSE-3: Compose renumbers requirement IDs into a clean sequential order
|
|
15
17
|
*/
|
|
16
18
|
import type { Outline } from "./schemas.js";
|
|
17
19
|
export interface ComposeOptions {
|
|
@@ -43,7 +45,8 @@ export declare function assembleComposedDocument(outline: Outline, partialPathFo
|
|
|
43
45
|
partialsMissing: number;
|
|
44
46
|
};
|
|
45
47
|
/**
|
|
46
|
-
* Compose the spec and write it to disk.
|
|
48
|
+
* Compose the spec and write it to disk. Renumbers requirement IDs into a
|
|
49
|
+
* clean sequence (CTS-COMPOSE-3), then validates the result.
|
|
47
50
|
*/
|
|
48
51
|
export declare function runCompose(options: ComposeOptions): ComposeResult;
|
|
49
52
|
//# sourceMappingURL=compose.d.ts.map
|
|
@@ -5,17 +5,20 @@
|
|
|
5
5
|
* title, a summary paragraph from the outline, and one H2 section per area.
|
|
6
6
|
* Each area section contains the partial's content.
|
|
7
7
|
*
|
|
8
|
-
* After assembly,
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* After assembly, a deterministic renumbering pass rewrites each prefix's
|
|
9
|
+
* requirement IDs into a clean 1..N sequence (sweeping the ID-sequence scars
|
|
10
|
+
* specifier/edit rounds leave behind), then schema validation runs over the
|
|
11
|
+
* document. Errors validation can't resolve surface to the caller.
|
|
11
12
|
*
|
|
12
13
|
* Requirements covered:
|
|
13
14
|
* - CTS-COMPOSE-1: Compose produces a single Markdown document from partials
|
|
14
15
|
* - CTS-COMPOSE-2: Compose validates the resulting document and corrects mechanical errors
|
|
16
|
+
* - CTS-COMPOSE-3: Compose renumbers requirement IDs into a clean sequential order
|
|
15
17
|
*/
|
|
16
18
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
17
19
|
import { parseRequirementsFile } from "../schema/parser.js";
|
|
18
|
-
import { sanitizeAreaName } from "./
|
|
20
|
+
import { sanitizeAreaName } from "./area-name.js";
|
|
21
|
+
import { renumberRequirements } from "./renumber.js";
|
|
19
22
|
/**
|
|
20
23
|
* Read a partial file. If missing or marked as a missing-partial placeholder,
|
|
21
24
|
* return null (caller will emit a stub section).
|
|
@@ -84,27 +87,13 @@ function escapeYamlString(s) {
|
|
|
84
87
|
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
85
88
|
}
|
|
86
89
|
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
* For v0, the simplest mechanical fix is duplicate IDs within an area —
|
|
90
|
-
* we detect and renumber them sequentially. Position-path and Gherkin syntax
|
|
91
|
-
* errors surface to the validator's caller; we don't auto-fix those because
|
|
92
|
-
* the editor loop is better suited to it.
|
|
93
|
-
*/
|
|
94
|
-
function fixMechanicalErrors(content) {
|
|
95
|
-
// Detect duplicate IDs (lines like "PREFIX-AREA-001: ...") within each area.
|
|
96
|
-
// For v0 we punt — the schema validator will catch them and the caller will
|
|
97
|
-
// surface to the user. Returning content unchanged keeps the contract: the
|
|
98
|
-
// function is a no-op for inputs without fixable errors.
|
|
99
|
-
return content;
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Compose the spec and write it to disk. Validates the result.
|
|
90
|
+
* Compose the spec and write it to disk. Renumbers requirement IDs into a
|
|
91
|
+
* clean sequence (CTS-COMPOSE-3), then validates the result.
|
|
103
92
|
*/
|
|
104
93
|
export function runCompose(options) {
|
|
105
94
|
const { outline, partialPathFor, composedPath } = options;
|
|
106
95
|
const { content, partialsIncluded, partialsMissing } = assembleComposedDocument(outline, partialPathFor);
|
|
107
|
-
const finalContent =
|
|
96
|
+
const finalContent = renumberRequirements(content);
|
|
108
97
|
writeFileSync(composedPath, finalContent, "utf-8");
|
|
109
98
|
// Validate
|
|
110
99
|
let validationError;
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
*/
|
|
30
30
|
import { existsSync, readFileSync } from "node:fs";
|
|
31
31
|
import { findProjectRoot } from "../utils/project-settings.js";
|
|
32
|
+
import { sanitizeAreaName } from "./area-name.js";
|
|
32
33
|
import { cachePaths } from "./cache.js";
|
|
33
|
-
import { sanitizeAreaName } from "./fan-out.js";
|
|
34
34
|
import { EDITOR_PROMPT } from "./prompts/editor.js";
|
|
35
35
|
import { PLANNER_INITIAL_PROMPT } from "./prompts/planner-initial.js";
|
|
36
36
|
import { PLANNER_REVISE_PROMPT } from "./prompts/planner-revise.js";
|
|
@@ -17,8 +17,9 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
17
17
|
import { dirname, join } from "node:path";
|
|
18
18
|
import { parseRequirementsFile } from "../schema/parser.js";
|
|
19
19
|
import { generateRunMarker } from "../schema/run-marker.js";
|
|
20
|
-
import { sanitizeAreaName } from "./
|
|
20
|
+
import { sanitizeAreaName } from "./area-name.js";
|
|
21
21
|
import { promptOverwriteChoice } from "./interactive.js";
|
|
22
|
+
import { renumberRequirements } from "./renumber.js";
|
|
22
23
|
export const AREA_SPLIT_THRESHOLD = 5;
|
|
23
24
|
/**
|
|
24
25
|
* Resolve the output path(s) the present stage would write to.
|
|
@@ -223,7 +224,11 @@ function validateContent(content) {
|
|
|
223
224
|
*/
|
|
224
225
|
export async function runPresent(options) {
|
|
225
226
|
const { outline, finalSpecPath, projectRoot, overwritePolicy, outputSubdir } = options;
|
|
226
|
-
|
|
227
|
+
// Renumber IDs one last time before writing out (CTS-COMPOSE-3.5): the
|
|
228
|
+
// cross-area review/edit pass that runs between compose and present can
|
|
229
|
+
// reintroduce ID-sequence scars (merges leave gaps, edits add suffixes), so
|
|
230
|
+
// this sweep guarantees the presented spec carries a clean sequence.
|
|
231
|
+
const composedSpec = renumberRequirements(readFileSync(finalSpecPath, "utf-8"));
|
|
227
232
|
const plan = planPresentPaths(outline, projectRoot, outputSubdir);
|
|
228
233
|
// Compute the content for each output up front.
|
|
229
234
|
// IMPORT-1.0/1.1: one run marker per present invocation, stamped into every
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* System prompt for the initial planner pass.
|
|
3
3
|
*
|
|
4
|
-
* Reads a compressed packed view of a codebase and produces
|
|
5
|
-
*
|
|
4
|
+
* Reads a compressed packed view of a codebase and produces the behavioral-area
|
|
5
|
+
* outline. The worker dispatch supplies the exact output format and path; the
|
|
6
|
+
* outline's shape is the `CTS-PLAN-1` contract.
|
|
6
7
|
*
|
|
7
8
|
* Requirements covered:
|
|
8
9
|
* - CTS-PLAN-1: Planner produces a behavioral outline from the compressed pack
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* System prompt for the initial planner pass.
|
|
3
3
|
*
|
|
4
|
-
* Reads a compressed packed view of a codebase and produces
|
|
5
|
-
*
|
|
4
|
+
* Reads a compressed packed view of a codebase and produces the behavioral-area
|
|
5
|
+
* outline. The worker dispatch supplies the exact output format and path; the
|
|
6
|
+
* outline's shape is the `CTS-PLAN-1` contract.
|
|
6
7
|
*
|
|
7
8
|
* Requirements covered:
|
|
8
9
|
* - CTS-PLAN-1: Planner produces a behavioral outline from the compressed pack
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic requirement-ID renumbering for composed CTS specs.
|
|
3
|
+
*
|
|
4
|
+
* Review and edit rounds leave ID-sequence scars behind: out-of-order,
|
|
5
|
+
* skipped, and letter-suffixed requirement numbers (`HONO-CTX-50` wedged
|
|
6
|
+
* between 16 and 17, `AUTH-PWD-12B`, `CUTIL-11a`). They are functionally
|
|
7
|
+
* harmless but read as sloppiness. This module rewrites each prefix's IDs
|
|
8
|
+
* into a clean, contiguous 1..N sequence in document order, and rewrites prose
|
|
9
|
+
* cross-references to match.
|
|
10
|
+
*
|
|
11
|
+
* It runs at two deterministic chokepoints (CTS-COMPOSE-3.0, CTS-COMPOSE-3.5):
|
|
12
|
+
* - compose, before the cross-area review pass reads the spec (so reviewers
|
|
13
|
+
* cite final IDs);
|
|
14
|
+
* - present, after that pass, so scars the review/edit pass introduces are
|
|
15
|
+
* swept before the spec is written out.
|
|
16
|
+
*
|
|
17
|
+
* It operates on raw text rather than the parsed tree on purpose: the dirty
|
|
18
|
+
* input (letter-suffixed keys) fails strict key validation in the parser — the
|
|
19
|
+
* scars are exactly what we need to clean before validation can pass. Working
|
|
20
|
+
* on text also keeps the blast radius tiny: only ID tokens change, everything
|
|
21
|
+
* else (prose, formatting, position paths) is preserved.
|
|
22
|
+
*
|
|
23
|
+
* Requirements covered:
|
|
24
|
+
* - CTS-COMPOSE-3: Compose renumbers requirement IDs into a clean sequential order
|
|
25
|
+
*/
|
|
26
|
+
export interface Rename {
|
|
27
|
+
/** The original ID exactly as it appears in the document. */
|
|
28
|
+
from: string;
|
|
29
|
+
/** The clean, sequential replacement ID. */
|
|
30
|
+
to: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Scan top-level requirement headers in document order and assign each prefix
|
|
34
|
+
* a contiguous 1..N sequence. Returns the rename list (including identity
|
|
35
|
+
* renames for already-clean IDs, so callers can treat the result uniformly).
|
|
36
|
+
*
|
|
37
|
+
* Only the first line of each ```dotrequirements block is treated as a header
|
|
38
|
+
* (one requirement per block, matching the parser's contract). Exact-duplicate
|
|
39
|
+
* keys are a distinct validation error and out of scope here: a repeated key
|
|
40
|
+
* does not consume a sequence slot (so it introduces no phantom gap) and keeps
|
|
41
|
+
* its first mapping, leaving the downstream validator to flag the duplicate.
|
|
42
|
+
*/
|
|
43
|
+
export declare function computeRenames(content: string): Rename[];
|
|
44
|
+
/**
|
|
45
|
+
* Renumber every requirement ID in a composed spec into a clean, contiguous
|
|
46
|
+
* 1..N sequence per prefix, rewriting prose cross-references to match.
|
|
47
|
+
*
|
|
48
|
+
* Deterministic and idempotent: on an already-clean spec the rename map is the
|
|
49
|
+
* identity, so the output is byte-for-byte identical (CTS-COMPOSE-3.6).
|
|
50
|
+
*/
|
|
51
|
+
export declare function renumberRequirements(content: string): string;
|
|
52
|
+
//# sourceMappingURL=renumber.d.ts.map
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic requirement-ID renumbering for composed CTS specs.
|
|
3
|
+
*
|
|
4
|
+
* Review and edit rounds leave ID-sequence scars behind: out-of-order,
|
|
5
|
+
* skipped, and letter-suffixed requirement numbers (`HONO-CTX-50` wedged
|
|
6
|
+
* between 16 and 17, `AUTH-PWD-12B`, `CUTIL-11a`). They are functionally
|
|
7
|
+
* harmless but read as sloppiness. This module rewrites each prefix's IDs
|
|
8
|
+
* into a clean, contiguous 1..N sequence in document order, and rewrites prose
|
|
9
|
+
* cross-references to match.
|
|
10
|
+
*
|
|
11
|
+
* It runs at two deterministic chokepoints (CTS-COMPOSE-3.0, CTS-COMPOSE-3.5):
|
|
12
|
+
* - compose, before the cross-area review pass reads the spec (so reviewers
|
|
13
|
+
* cite final IDs);
|
|
14
|
+
* - present, after that pass, so scars the review/edit pass introduces are
|
|
15
|
+
* swept before the spec is written out.
|
|
16
|
+
*
|
|
17
|
+
* It operates on raw text rather than the parsed tree on purpose: the dirty
|
|
18
|
+
* input (letter-suffixed keys) fails strict key validation in the parser — the
|
|
19
|
+
* scars are exactly what we need to clean before validation can pass. Working
|
|
20
|
+
* on text also keeps the blast radius tiny: only ID tokens change, everything
|
|
21
|
+
* else (prose, formatting, position paths) is preserved.
|
|
22
|
+
*
|
|
23
|
+
* Requirements covered:
|
|
24
|
+
* - CTS-COMPOSE-3: Compose renumbers requirement IDs into a clean sequential order
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* A top-level requirement header line: `PREFIX-NUM[suffix]: ...`.
|
|
28
|
+
* The prefix is letters/hyphens (no digits — digits only appear in the number),
|
|
29
|
+
* so the first `-<digits>` after the prefix is unambiguously the numeric tail.
|
|
30
|
+
* A trailing letter run (`12B`, `11a`) is an editing scar we discard.
|
|
31
|
+
*/
|
|
32
|
+
const HEADER_RE = /^([A-Z][A-Z-]*?)-(\d+)([A-Za-z]*)\s*:/;
|
|
33
|
+
/** A ```dotrequirements fenced block; group 1 is the block body. Tolerates
|
|
34
|
+
* CRLF so a `\r\n`-encoded spec still renumbers rather than silently no-op'ing. */
|
|
35
|
+
const BLOCK_RE = /```dotrequirements\r?\n([\s\S]*?)```/g;
|
|
36
|
+
function escapeRegExp(s) {
|
|
37
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Scan top-level requirement headers in document order and assign each prefix
|
|
41
|
+
* a contiguous 1..N sequence. Returns the rename list (including identity
|
|
42
|
+
* renames for already-clean IDs, so callers can treat the result uniformly).
|
|
43
|
+
*
|
|
44
|
+
* Only the first line of each ```dotrequirements block is treated as a header
|
|
45
|
+
* (one requirement per block, matching the parser's contract). Exact-duplicate
|
|
46
|
+
* keys are a distinct validation error and out of scope here: a repeated key
|
|
47
|
+
* does not consume a sequence slot (so it introduces no phantom gap) and keeps
|
|
48
|
+
* its first mapping, leaving the downstream validator to flag the duplicate.
|
|
49
|
+
*/
|
|
50
|
+
export function computeRenames(content) {
|
|
51
|
+
const counters = new Map();
|
|
52
|
+
const seen = new Set();
|
|
53
|
+
const renames = [];
|
|
54
|
+
BLOCK_RE.lastIndex = 0;
|
|
55
|
+
let block = BLOCK_RE.exec(content);
|
|
56
|
+
while (block !== null) {
|
|
57
|
+
const body = block[1];
|
|
58
|
+
const firstLine = body.split("\n").find((l) => l.trim().length > 0) ?? "";
|
|
59
|
+
const m = firstLine.match(HEADER_RE);
|
|
60
|
+
if (m) {
|
|
61
|
+
const [, prefix, num, suffix] = m;
|
|
62
|
+
const from = `${prefix}-${num}${suffix}`;
|
|
63
|
+
if (!seen.has(from)) {
|
|
64
|
+
seen.add(from);
|
|
65
|
+
const next = (counters.get(prefix) ?? 0) + 1;
|
|
66
|
+
counters.set(prefix, next);
|
|
67
|
+
renames.push({ from, to: `${prefix}-${next}` });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
block = BLOCK_RE.exec(content);
|
|
71
|
+
}
|
|
72
|
+
return renames;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Renumber every requirement ID in a composed spec into a clean, contiguous
|
|
76
|
+
* 1..N sequence per prefix, rewriting prose cross-references to match.
|
|
77
|
+
*
|
|
78
|
+
* Deterministic and idempotent: on an already-clean spec the rename map is the
|
|
79
|
+
* identity, so the output is byte-for-byte identical (CTS-COMPOSE-3.6).
|
|
80
|
+
*/
|
|
81
|
+
export function renumberRequirements(content) {
|
|
82
|
+
const renames = computeRenames(content);
|
|
83
|
+
if (renames.length === 0)
|
|
84
|
+
return content;
|
|
85
|
+
const map = new Map(renames.map((r) => [r.from, r.to]));
|
|
86
|
+
// One combined, single-pass replacement so each ID location is rewritten
|
|
87
|
+
// exactly once — no risk of a freshly-written new ID being re-matched.
|
|
88
|
+
// Longest-first alternation so a shorter key can't shadow a longer one that
|
|
89
|
+
// shares its text. The lookbehind rejects a preceding letter/digit/hyphen so
|
|
90
|
+
// we never match the tail of a longer hyphenated token (e.g. `CTX-5` inside
|
|
91
|
+
// `HONO-CTX-5`); the lookahead mirrors it, rejecting a following
|
|
92
|
+
// letter/digit/hyphen so `AUTH-3` never matches inside `AUTH-30`, `AUTH-3B`,
|
|
93
|
+
// or a hyphen-joined token, while a following `.` (a child position path like
|
|
94
|
+
// `AUTH-3.2`) is preserved.
|
|
95
|
+
const alternation = renames
|
|
96
|
+
.map((r) => r.from)
|
|
97
|
+
.sort((a, b) => b.length - a.length)
|
|
98
|
+
.map(escapeRegExp)
|
|
99
|
+
.join("|");
|
|
100
|
+
const re = new RegExp(`(?<![A-Za-z0-9-])(${alternation})(?![A-Za-z0-9-])`, "g");
|
|
101
|
+
// Cross-references whose ID isn't an existing top-level requirement aren't in
|
|
102
|
+
// the map and so are never matched — they're left unchanged (CTS-COMPOSE-3.4).
|
|
103
|
+
return content.replace(re, (matched) => map.get(matched) ?? matched);
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=renumber.js.map
|