gentle-pi 0.3.3 → 0.3.5

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.
@@ -0,0 +1,99 @@
1
+ import { existsSync, readdirSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { parseDeltaSpec } from "./openspec-deltas.ts";
4
+
5
+ export interface DomainCollision {
6
+ change: string;
7
+ path: string;
8
+ }
9
+
10
+ export interface LegacyFlatSpecWarning {
11
+ change: string;
12
+ path: string;
13
+ hasDomainSpecs: boolean;
14
+ }
15
+
16
+ export interface LargeModifiedRequirement {
17
+ name: string;
18
+ lineCount: number;
19
+ }
20
+
21
+ export interface DestructiveDeltaReport {
22
+ destructive: boolean;
23
+ removedRequirements: string[];
24
+ largeModifiedRequirements: LargeModifiedRequirement[];
25
+ }
26
+
27
+ export interface DestructiveDeltaOptions {
28
+ largeModifiedLineThreshold?: number;
29
+ }
30
+
31
+ function safeDirectories(path: string): string[] {
32
+ try {
33
+ return readdirSync(path).filter((entry) => {
34
+ try {
35
+ return statSync(join(path, entry)).isDirectory();
36
+ } catch {
37
+ return false;
38
+ }
39
+ });
40
+ } catch {
41
+ return [];
42
+ }
43
+ }
44
+
45
+ function hasAnyDomainSpec(specsDir: string): boolean {
46
+ for (const domain of safeDirectories(specsDir)) {
47
+ if (existsSync(join(specsDir, domain, "spec.md"))) return true;
48
+ }
49
+ return false;
50
+ }
51
+
52
+ export function detectActiveDomainCollisions(
53
+ cwd: string,
54
+ changeName: string,
55
+ domain: string,
56
+ ): DomainCollision[] {
57
+ const changesDir = join(cwd, "openspec", "changes");
58
+ const collisions: DomainCollision[] = [];
59
+ for (const change of safeDirectories(changesDir)) {
60
+ if (change === "archive" || change === changeName) continue;
61
+ const path = join(changesDir, change, "specs", domain, "spec.md");
62
+ if (existsSync(path)) collisions.push({ change, path });
63
+ }
64
+ return collisions;
65
+ }
66
+
67
+ export function detectLegacyFlatSpec(
68
+ cwd: string,
69
+ changeName: string,
70
+ ): LegacyFlatSpecWarning | undefined {
71
+ const changeDir = join(cwd, "openspec", "changes", changeName);
72
+ const path = join(changeDir, "spec.md");
73
+ if (!existsSync(path)) return undefined;
74
+ return {
75
+ change: changeName,
76
+ path,
77
+ hasDomainSpecs: hasAnyDomainSpec(join(changeDir, "specs")),
78
+ };
79
+ }
80
+
81
+ export function analyzeDeltaDestructiveness(
82
+ deltaMarkdown: string,
83
+ options: DestructiveDeltaOptions = {},
84
+ ): DestructiveDeltaReport {
85
+ const threshold = options.largeModifiedLineThreshold ?? 40;
86
+ const delta = parseDeltaSpec(deltaMarkdown);
87
+ const removedRequirements = delta.removed.map((block) => block.name);
88
+ const largeModifiedRequirements = delta.modified
89
+ .map((block) => ({
90
+ name: block.name,
91
+ lineCount: block.content.split("\n").length,
92
+ }))
93
+ .filter((block) => block.lineCount >= threshold);
94
+ return {
95
+ destructive: removedRequirements.length > 0 || largeModifiedRequirements.length > 0,
96
+ removedRequirements,
97
+ largeModifiedRequirements,
98
+ };
99
+ }
@@ -1,4 +1,5 @@
1
1
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
2
3
  import { dirname, join } from "node:path";
3
4
  import { fileURLToPath } from "node:url";
4
5
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
@@ -6,6 +7,10 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
6
7
  const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
7
8
  const ASSETS_DIR = join(PACKAGE_ROOT, "assets");
8
9
 
10
+ function gentlePiAgentHome(): string {
11
+ return process.env.GENTLE_PI_AGENT_HOME ?? join(homedir(), ".pi", "agent");
12
+ }
13
+
9
14
  export type SddExecutionMode = "interactive" | "auto";
10
15
  export type SddArtifactStore = "openspec" | "engram" | "both";
11
16
  export type SddChainedPrStrategy =
@@ -89,22 +94,23 @@ function copyDirectoryFiles(
89
94
  }
90
95
 
91
96
  export function installSddAssets(
92
- cwd: string,
97
+ _cwd: string,
93
98
  force: boolean,
94
99
  ): { agents: number; chains: number; support: number; skipped: number } {
100
+ const agentHome = gentlePiAgentHome();
95
101
  const agents = copyDirectoryFiles(
96
102
  join(ASSETS_DIR, "agents"),
97
- join(cwd, ".pi", "agents"),
103
+ join(agentHome, "agents"),
98
104
  force,
99
105
  );
100
106
  const chains = copyDirectoryFiles(
101
107
  join(ASSETS_DIR, "chains"),
102
- join(cwd, ".pi", "chains"),
108
+ join(agentHome, "chains"),
103
109
  force,
104
110
  );
105
111
  const support = copyDirectoryFiles(
106
112
  join(ASSETS_DIR, "support"),
107
- join(cwd, ".pi", "gentle-ai", "support"),
113
+ join(agentHome, "gentle-ai", "support"),
108
114
  force,
109
115
  );
110
116
  return {
@@ -248,7 +254,7 @@ export async function ensureSddPreflight(
248
254
  `Artifacts: ${prefs.artifactStore}`,
249
255
  `PR chaining: ${prefs.chainedPrStrategy}`,
250
256
  `Review budget: ${prefs.reviewBudgetLines} changed lines`,
251
- `Assets installed: ${result.agents} agent(s), ${result.chains} chain(s), ${result.support} support file(s), ${result.skipped} skipped.`,
257
+ `Global SDD assets ready: ${result.agents} agent(s), ${result.chains} chain(s), ${result.support} support file(s), ${result.skipped} already present.`,
252
258
  modelRoutingLine,
253
259
  ].join("\n"),
254
260
  modelResult.invalidPath ? "warning" : "info",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gentle-pi",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "Turn Pi into el Gentleman: a senior-architect development harness with SDD/OpenSpec, subagents, strict TDD evidence, review guardrails, and skill discovery.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -6,9 +6,21 @@ import { fileURLToPath } from "node:url";
6
6
  const root = join(fileURLToPath(new URL("..", import.meta.url)));
7
7
 
8
8
  const requiredPaths = [
9
+ "assets/agents/sdd-apply.md",
10
+ "assets/agents/sdd-archive.md",
11
+ "assets/agents/sdd-design.md",
12
+ "assets/agents/sdd-explore.md",
9
13
  "assets/agents/sdd-init.md",
14
+ "assets/agents/sdd-proposal.md",
15
+ "assets/agents/sdd-spec.md",
16
+ "assets/agents/sdd-sync.md",
17
+ "assets/agents/sdd-tasks.md",
18
+ "assets/agents/sdd-verify.md",
19
+ "assets/chains/sdd-full.chain.md",
10
20
  "assets/chains/sdd-plan.chain.md",
21
+ "assets/chains/sdd-verify.chain.md",
11
22
  "assets/support/strict-tdd.md",
23
+ "assets/support/strict-tdd-verify.md",
12
24
  "extensions/gentle-ai.ts",
13
25
  "extensions/sdd-init.ts",
14
26
  "extensions/skill-registry.ts",
@@ -55,4 +55,4 @@ Hard delegation triggers:
55
55
  - **Incident rule**: after wrong cwd, accidental worktree/repo mutation, merge recovery, confusing test command, or environment workaround, run fresh audit.
56
56
  - **Long-session rule**: after roughly 20 tool calls, 5 exploratory reads, or 2 non-mechanical edits with no delegation and accumulating complexity, pause and choose a subagent or justify not doing so.
57
57
 
58
- The package auto-installs SDD agents and chains into the project when a Pi session starts. Use `/gentle-ai:install-sdd --force` only for recovery or intentional overwrite.
58
+ The package ensures SDD agents and chains are available as global Pi runtime assets. Project-local SDD files are overrides/debug copies only. Use `/gentle-ai:install-sdd --force` only for recovery or intentional global refresh.
@@ -13,7 +13,7 @@ Load this skill only when the user explicitly asks for Judgment Day, dual/advers
13
13
 
14
14
  ## Hard Rules
15
15
 
16
- - Resolve project skills before launching agents: read skill registry, match compact rules by target files/task, and inject the same `Project Standards` block into both judge prompts and fix prompts.
16
+ - Resolve project skills before launching agents: read skill registry, match indexed paths by target files/task, and pass the same `Skills to load before work` block into both judge prompts and fix prompts.
17
17
  - Launch **two blind judges in parallel** with identical target and criteria; never review the code yourself.
18
18
  - Wait for both judges before synthesis; never accept a partial verdict.
19
19
  - Classify warnings as `WARNING (real)` only if normal intended use can trigger them; otherwise downgrade to INFO as `WARNING (theoretical)`.
@@ -8,8 +8,8 @@ You are an adversarial code reviewer. Your ONLY job is to find problems.
8
8
  ## Target
9
9
  {files, feature, architecture, component}
10
10
 
11
- ## Project Standards (auto-resolved)
12
- {matching compact rules, if available}
11
+ ## Skills to load before work
12
+ {matching SKILL.md paths, if available}
13
13
 
14
14
  ## Review Criteria
15
15
  - Correctness: logical errors and behavior mismatches
@@ -33,7 +33,7 @@ WARNING rule: normal intended use can trigger it → `WARNING (real)`; contrived
33
33
 
34
34
  If clean: `VERDICT: CLEAN — No issues found.`
35
35
 
36
- Always end with: `Skill Resolution: {injected|fallback-registry|fallback-path|none} — {details}`.
36
+ Always end with: `Skill Resolution: {paths-injected|fallback-registry|fallback-path|none} — {details}`.
37
37
  ```
38
38
 
39
39
  ## Fix Agent Prompt
@@ -44,8 +44,8 @@ You are a surgical fix agent. Apply ONLY the confirmed issues listed below.
44
44
  ## Confirmed Issues to Fix
45
45
  {confirmed findings table}
46
46
 
47
- ## Project Standards (auto-resolved)
48
- {matching compact rules, if available}
47
+ ## Skills to load before work
48
+ {matching SKILL.md paths, if available}
49
49
 
50
50
  ## Instructions
51
51
  - Fix only confirmed issues.
@@ -54,7 +54,7 @@ You are a surgical fix agent. Apply ONLY the confirmed issues listed below.
54
54
  - If fixing a repeated pattern in touched files, fix all occurrences of that same pattern.
55
55
  - Return changed file, line, and fix summary.
56
56
 
57
- End with: `Skill Resolution: {injected|fallback-registry|fallback-path|none} — {details}`.
57
+ End with: `Skill Resolution: {paths-injected|fallback-registry|fallback-path|none} — {details}`.
58
58
  ```
59
59
 
60
60
  ## Verdict Table
@@ -0,0 +1,51 @@
1
+ ---
2
+ name: skill-registry
3
+ description: "Trigger: update skills, skill registry, actualizar skills, after skill changes. Index available skills by trigger and path."
4
+ license: MIT
5
+ metadata:
6
+ author: gentleman-programming
7
+ version: "1.0"
8
+ ---
9
+
10
+ ## Activation Contract
11
+
12
+ Use this skill after installing, removing, creating, moving, or renaming skills, or when a delegator needs a fresh skill index.
13
+
14
+ ## Hard Rules
15
+
16
+ - The registry is an index, not a compiler or summary. `SKILL.md` remains the source of truth.
17
+ - Do not generate or inject compact rules by default; preserve author intent by passing exact skill paths to subagents.
18
+ - Always write `.atl/skill-registry.md` regardless of SDD persistence mode.
19
+ - Save the registry to Engram as `topic_key: skill-registry` when available, with `capture_prompt: false`.
20
+ - Skip `sdd-*`, `_shared`, and `skill-registry`; deduplicate by skill name, preferring project-level skills over user-level skills.
21
+ - Add `.atl/` to `.gitignore` when possible unless explicitly disabled.
22
+
23
+ ## Decision Gates
24
+
25
+ | Situation | Action |
26
+ | --- | --- |
27
+ | Same skill exists globally and in project | Keep the project-level skill |
28
+ | Same skill exists in multiple global locations | Keep the first source in scan order |
29
+ | No skills found | Write an empty registry so agents stop searching blindly |
30
+ | Agent will delegate work | Select matching registry rows and pass their `SKILL.md` paths |
31
+
32
+ ## Execution Steps
33
+
34
+ 1. Scan all known user and project skill directories for `*/SKILL.md`.
35
+ 2. Read frontmatter only as needed to extract `name` and `description` trigger text.
36
+ 3. Render `.atl/skill-registry.md` with scanned sources, registry contract, skill name, trigger/description, scope, and exact path.
37
+ 4. Persist to Engram when available using `title: skill-registry`, `topic_key: skill-registry`, `type: config`, and `capture_prompt: false`.
38
+ 5. Return the registry path, skill count, cache status, and whether Engram was updated.
39
+
40
+ ## Output Contract
41
+
42
+ Return:
43
+ - Project name and `.atl/skill-registry.md` path.
44
+ - Number of indexed skills.
45
+ - Whether the cache was hit or regenerated.
46
+ - Any skipped or duplicate skills when relevant.
47
+
48
+ ## References
49
+
50
+ - `docs/skill-style-guide.md` — how skills should be authored before indexing.
51
+ - `skills/_shared/skill-resolver.md` — how delegators use the index.
@@ -0,0 +1,209 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import {
4
+ applyDeltaSpec,
5
+ parseDeltaSpec,
6
+ parseRequirementBlocks,
7
+ } from "../lib/openspec-deltas.ts";
8
+
9
+ const canonicalSpec = `# Example Specification
10
+
11
+ ## Purpose
12
+
13
+ Example domain.
14
+
15
+ ## Requirements
16
+
17
+ ### Requirement: Existing Behavior
18
+
19
+ The system MUST keep existing behavior.
20
+
21
+ #### Scenario: Happy path
22
+
23
+ - GIVEN an existing condition
24
+ - WHEN the action runs
25
+ - THEN existing behavior is preserved
26
+
27
+ ---
28
+
29
+ ### Requirement: Deprecated Behavior
30
+
31
+ The system MUST support old behavior.
32
+
33
+ #### Scenario: Old path
34
+
35
+ - GIVEN an old condition
36
+ - WHEN the action runs
37
+ - THEN old behavior is preserved
38
+ `;
39
+
40
+ const deltaSpec = `# Delta for Example
41
+
42
+ ## ADDED Requirements
43
+
44
+ ### Requirement: New Behavior
45
+
46
+ The system MUST support new behavior.
47
+
48
+ #### Scenario: New path
49
+
50
+ - GIVEN a new condition
51
+ - WHEN the action runs
52
+ - THEN new behavior is available
53
+
54
+ ## MODIFIED Requirements
55
+
56
+ ### Requirement: Existing Behavior
57
+
58
+ The system MUST keep existing behavior and report audit evidence.
59
+ (Previously: existing behavior did not report audit evidence)
60
+
61
+ #### Scenario: Happy path
62
+
63
+ - GIVEN an existing condition
64
+ - WHEN the action runs
65
+ - THEN existing behavior is preserved
66
+ - AND audit evidence is recorded
67
+
68
+ ## REMOVED Requirements
69
+
70
+ ### Requirement: Deprecated Behavior
71
+
72
+ (Reason: old behavior is no longer supported)
73
+ `;
74
+
75
+ test("parseRequirementBlocks extracts requirement blocks with names", () => {
76
+ const blocks = parseRequirementBlocks(canonicalSpec);
77
+
78
+ assert.deepEqual(
79
+ blocks.map((block) => block.name),
80
+ ["Existing Behavior", "Deprecated Behavior"],
81
+ );
82
+ assert.match(blocks[0].content, /Scenario: Happy path/);
83
+ assert.match(blocks[1].content, /old behavior/i);
84
+ });
85
+
86
+ test("parseDeltaSpec extracts ADDED, MODIFIED, and REMOVED sections", () => {
87
+ const delta = parseDeltaSpec(deltaSpec);
88
+
89
+ assert.deepEqual(
90
+ delta.added.map((block) => block.name),
91
+ ["New Behavior"],
92
+ );
93
+ assert.deepEqual(
94
+ delta.modified.map((block) => block.name),
95
+ ["Existing Behavior"],
96
+ );
97
+ assert.deepEqual(
98
+ delta.removed.map((block) => block.name),
99
+ ["Deprecated Behavior"],
100
+ );
101
+ });
102
+
103
+ test("applyDeltaSpec applies ADDED, MODIFIED, and REMOVED while preserving unrelated content", () => {
104
+ const result = applyDeltaSpec(canonicalSpec, deltaSpec);
105
+
106
+ assert.match(result, /### Requirement: New Behavior/);
107
+ assert.match(result, /audit evidence is recorded/);
108
+ assert.doesNotMatch(result, /### Requirement: Deprecated Behavior/);
109
+ assert.match(result, /# Example Specification/);
110
+ assert.match(result, /## Purpose/);
111
+ assert.match(result, /## Requirements/);
112
+ });
113
+
114
+ test("applyDeltaSpec preserves sections after Requirements when appending ADDED", () => {
115
+ const result = applyDeltaSpec(
116
+ `${canonicalSpec}\n## Notes\n\nKeep this section.\n`,
117
+ `# Delta
118
+
119
+ ## ADDED Requirements
120
+
121
+ ### Requirement: New Behavior
122
+
123
+ The system MUST support new behavior.
124
+ `,
125
+ );
126
+
127
+ assert.match(result, /### Requirement: New Behavior[\s\S]*\n\n## Notes\n\nKeep this section\./);
128
+ assert.doesNotMatch(result, /Behavior## Notes/);
129
+ });
130
+
131
+ test("applyDeltaSpec does not duplicate separators between multiple ADDED requirements", () => {
132
+ const result = applyDeltaSpec(
133
+ canonicalSpec,
134
+ `# Delta
135
+
136
+ ## ADDED Requirements
137
+
138
+ ### Requirement: First New Behavior
139
+
140
+ The system MUST support the first behavior.
141
+
142
+ ---
143
+
144
+ ### Requirement: Second New Behavior
145
+
146
+ The system MUST support the second behavior.
147
+ `,
148
+ );
149
+
150
+ assert.match(result, /### Requirement: First New Behavior[\s\S]*---[\s\S]*### Requirement: Second New Behavior/);
151
+ assert.doesNotMatch(result, /---\n\n---/);
152
+ });
153
+
154
+ test("applyDeltaSpec rejects MODIFIED requirements that do not exist", () => {
155
+ assert.throws(
156
+ () =>
157
+ applyDeltaSpec(
158
+ canonicalSpec,
159
+ `# Delta
160
+
161
+ ## MODIFIED Requirements
162
+
163
+ ### Requirement: Missing Behavior
164
+
165
+ The system MUST fail.
166
+ `,
167
+ ),
168
+ /missing canonical requirement.*Missing Behavior/i,
169
+ );
170
+ });
171
+
172
+ test("applyDeltaSpec rejects REMOVED requirements that do not exist", () => {
173
+ assert.throws(
174
+ () =>
175
+ applyDeltaSpec(
176
+ canonicalSpec,
177
+ `# Delta
178
+
179
+ ## REMOVED Requirements
180
+
181
+ ### Requirement: Missing Behavior
182
+
183
+ (Reason: already absent)
184
+ `,
185
+ ),
186
+ /missing canonical requirement.*Missing Behavior/i,
187
+ );
188
+ });
189
+
190
+ test("applyDeltaSpec rejects duplicate operations for the same requirement", () => {
191
+ assert.throws(
192
+ () =>
193
+ parseDeltaSpec(`# Delta
194
+
195
+ ## ADDED Requirements
196
+
197
+ ### Requirement: Same Behavior
198
+
199
+ The system MUST do one thing.
200
+
201
+ ## REMOVED Requirements
202
+
203
+ ### Requirement: Same Behavior
204
+
205
+ (Reason: conflict)
206
+ `),
207
+ /duplicate delta operation.*Same Behavior/i,
208
+ );
209
+ });
@@ -0,0 +1,71 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdirSync, writeFileSync } from "node:fs";
3
+ import { mkdtemp } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import test from "node:test";
7
+ import {
8
+ analyzeDeltaDestructiveness,
9
+ detectActiveDomainCollisions,
10
+ detectLegacyFlatSpec,
11
+ } from "../lib/openspec-guardrails.ts";
12
+
13
+ test("detectActiveDomainCollisions finds other active changes touching the same domain", async () => {
14
+ const cwd = await mkdtemp(join(tmpdir(), "gentle-pi-guardrails-"));
15
+ mkdirSync(join(cwd, "openspec/changes/current/specs/sdd-openspec"), { recursive: true });
16
+ mkdirSync(join(cwd, "openspec/changes/other/specs/sdd-openspec"), { recursive: true });
17
+ mkdirSync(join(cwd, "openspec/changes/archive/2026-01-01-old/specs/sdd-openspec"), { recursive: true });
18
+ writeFileSync(join(cwd, "openspec/changes/current/specs/sdd-openspec/spec.md"), "# Current\n");
19
+ writeFileSync(join(cwd, "openspec/changes/other/specs/sdd-openspec/spec.md"), "# Other\n");
20
+ writeFileSync(join(cwd, "openspec/changes/archive/2026-01-01-old/specs/sdd-openspec/spec.md"), "# Old\n");
21
+
22
+ const collisions = detectActiveDomainCollisions(cwd, "current", "sdd-openspec");
23
+
24
+ assert.deepEqual(collisions.map((collision) => collision.change), ["other"]);
25
+ assert.match(collisions[0].path, /openspec\/changes\/other\/specs\/sdd-openspec\/spec\.md$/);
26
+ });
27
+
28
+ test("detectLegacyFlatSpec warns when a flat change spec exists without domain specs", async () => {
29
+ const cwd = await mkdtemp(join(tmpdir(), "gentle-pi-legacy-flat-"));
30
+ mkdirSync(join(cwd, "openspec/changes/legacy-change"), { recursive: true });
31
+ writeFileSync(join(cwd, "openspec/changes/legacy-change/spec.md"), "# Legacy\n");
32
+
33
+ assert.deepEqual(detectLegacyFlatSpec(cwd, "legacy-change"), {
34
+ change: "legacy-change",
35
+ path: join(cwd, "openspec/changes/legacy-change/spec.md"),
36
+ hasDomainSpecs: false,
37
+ });
38
+ });
39
+
40
+ test("detectLegacyFlatSpec reports domain specs when both old and new layouts exist", async () => {
41
+ const cwd = await mkdtemp(join(tmpdir(), "gentle-pi-legacy-both-"));
42
+ mkdirSync(join(cwd, "openspec/changes/mixed/specs/domain"), { recursive: true });
43
+ writeFileSync(join(cwd, "openspec/changes/mixed/spec.md"), "# Legacy\n");
44
+ writeFileSync(join(cwd, "openspec/changes/mixed/specs/domain/spec.md"), "# Domain\n");
45
+
46
+ assert.equal(detectLegacyFlatSpec(cwd, "mixed")?.hasDomainSpecs, true);
47
+ });
48
+
49
+ test("analyzeDeltaDestructiveness reports removed and large modified requirements", () => {
50
+ const report = analyzeDeltaDestructiveness(
51
+ `# Delta
52
+
53
+ ## MODIFIED Requirements
54
+
55
+ ### Requirement: Big Replacement
56
+
57
+ ${Array.from({ length: 15 }, (_, index) => `Line ${index + 1}`).join("\n")}
58
+
59
+ ## REMOVED Requirements
60
+
61
+ ### Requirement: Removed Behavior
62
+
63
+ (Reason: no longer supported)
64
+ `,
65
+ { largeModifiedLineThreshold: 10 },
66
+ );
67
+
68
+ assert.equal(report.destructive, true);
69
+ assert.deepEqual(report.removedRequirements, ["Removed Behavior"]);
70
+ assert.deepEqual(report.largeModifiedRequirements.map((item) => item.name), ["Big Replacement"]);
71
+ });