@warpgogol/forge 2.8.0 → 2.8.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.
Files changed (271) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.md +23 -13
  3. package/README.uk.md +19 -12
  4. package/dist/os/session/handlers/validate.d.ts.map +1 -1
  5. package/dist/os/session/handlers/validate.js +23 -1
  6. package/dist/os/session/handlers/validate.js.map +1 -1
  7. package/dist/os/session/types.d.ts +31 -1
  8. package/dist/os/session/types.d.ts.map +1 -1
  9. package/dist/os/session/types.js +7 -0
  10. package/dist/os/session/types.js.map +1 -1
  11. package/os/adr/adr.module.ts +158 -0
  12. package/os/adr/frontmatter-io.ts +80 -0
  13. package/os/adr/handlers/archive.ts +244 -0
  14. package/os/adr/handlers/implement-stamp.ts +331 -0
  15. package/os/adr/handlers/list-create.ts +207 -0
  16. package/os/adr/handlers/validate.test.ts +232 -0
  17. package/os/adr/handlers/validate.ts +429 -0
  18. package/os/adr/index.ts +33 -0
  19. package/os/adr/types.ts +141 -0
  20. package/os/audit/audit.module.ts +48 -0
  21. package/os/audit/frontmatter-io.ts +84 -0
  22. package/os/audit/handlers/archive.ts +239 -0
  23. package/os/audit/index.ts +23 -0
  24. package/os/audit/types.ts +29 -0
  25. package/os/compass/compass.module.ts +186 -0
  26. package/os/compass/handlers/compass-audit-handler.ts +382 -0
  27. package/os/compass/handlers/compass-change-summary-handler.ts +272 -0
  28. package/os/compass/handlers/compass-inventory-handler.ts +294 -0
  29. package/os/compass/handlers/compass-inventory.ts +521 -0
  30. package/os/compass/handlers/git-revision.ts +137 -0
  31. package/os/compass/handlers/resolve-scan-root-workpiece.test.ts +65 -0
  32. package/os/compass/handlers/resolve-scan-root.ts +79 -0
  33. package/os/compass/index.ts +33 -0
  34. package/os/core/core.module.ts +948 -0
  35. package/os/core/handlers/assets-check.ts +122 -0
  36. package/os/core/handlers/assets-helpers.ts +174 -0
  37. package/os/core/handlers/assets-list.ts +99 -0
  38. package/os/core/handlers/build.ts +155 -0
  39. package/os/core/handlers/determinism-check.ts +354 -0
  40. package/os/core/handlers/dev.ts +127 -0
  41. package/os/core/handlers/invariant-engine.test.ts +681 -0
  42. package/os/core/handlers/knowledge-compact.ts +248 -0
  43. package/os/core/handlers/lifecycle-handlers.test.ts +524 -0
  44. package/os/core/handlers/note-frontmatter-validate.test.ts +93 -0
  45. package/os/core/handlers/note-link-validate.test.ts +117 -0
  46. package/os/core/handlers/note-orphan-detect.test.ts +94 -0
  47. package/os/core/handlers/package-health.test.ts +226 -0
  48. package/os/core/handlers/package-health.ts +231 -0
  49. package/os/core/handlers/pinned-check.ts +200 -0
  50. package/os/core/handlers/pinned-init.ts +333 -0
  51. package/os/core/handlers/pinned-types.ts +50 -0
  52. package/os/core/handlers/pinned-validate.ts +301 -0
  53. package/os/core/handlers/profile-resolve.ts +92 -0
  54. package/os/core/handlers/release-prepare.ts +294 -0
  55. package/os/core/handlers/release-publish.ts +211 -0
  56. package/os/core/handlers/validate.ts +262 -0
  57. package/os/core/handlers/workspace-deps.ts +55 -0
  58. package/os/core/index.ts +13 -0
  59. package/os/exploration/exploration.module.ts +90 -0
  60. package/os/exploration/frontmatter-io.ts +77 -0
  61. package/os/exploration/handlers/archive.ts +159 -0
  62. package/os/exploration/handlers/list.ts +77 -0
  63. package/os/exploration/handlers/show.ts +107 -0
  64. package/os/exploration/index.ts +25 -0
  65. package/os/exploration/types.ts +65 -0
  66. package/os/mission/handlers/archive.test.ts +391 -0
  67. package/os/mission/handlers/archive.ts +453 -0
  68. package/os/mission/index.ts +1 -0
  69. package/os/mission/mission.module.ts +48 -0
  70. package/os/mission/types.ts +41 -0
  71. package/os/naming/index.ts +14 -0
  72. package/os/naming/naming-convention.test.ts +163 -0
  73. package/os/naming/naming-convention.ts +378 -0
  74. package/os/naming/naming.module.ts +36 -0
  75. package/os/notes/index.ts +10 -0
  76. package/os/notes/notes.module.ts +109 -0
  77. package/os/plan/frontmatter-io.ts +82 -0
  78. package/os/plan/handlers/archive.ts +238 -0
  79. package/os/plan/index.ts +23 -0
  80. package/os/plan/plan.module.ts +48 -0
  81. package/os/plan/types.ts +29 -0
  82. package/os/program/discovery.ts +294 -0
  83. package/os/program/handlers/complete.ts +419 -0
  84. package/os/program/handlers/lease.ts +433 -0
  85. package/os/program/handlers/seal.ts +332 -0
  86. package/os/program/handlers/validate.ts +213 -0
  87. package/os/program/lease.ts +156 -0
  88. package/os/program/program.module.ts +212 -0
  89. package/os/program/schemas.ts +218 -0
  90. package/os/program/state.ts +264 -0
  91. package/os/rfc/acceptance.ts +344 -0
  92. package/os/rfc/decision-log.ts +310 -0
  93. package/os/rfc/dna-trace.ts +280 -0
  94. package/os/rfc/frontmatter-io.test.ts +132 -0
  95. package/os/rfc/frontmatter-io.ts +130 -0
  96. package/os/rfc/handlers/archive.ts +300 -0
  97. package/os/rfc/handlers/check.ts +238 -0
  98. package/os/rfc/handlers/implement-stamp.ts +515 -0
  99. package/os/rfc/handlers/index-graph.ts +237 -0
  100. package/os/rfc/handlers/lifecycle.test.ts +127 -0
  101. package/os/rfc/handlers/lifecycle.ts +249 -0
  102. package/os/rfc/handlers/list-create.ts +296 -0
  103. package/os/rfc/handlers/pipeline-status.ts +173 -0
  104. package/os/rfc/handlers/shared.ts +121 -0
  105. package/os/rfc/handlers/supersede-propose.ts +241 -0
  106. package/os/rfc/handlers/validate-rules.test.ts +891 -0
  107. package/os/rfc/handlers/validate-rules.ts +984 -0
  108. package/os/rfc/handlers/validate.ts +166 -0
  109. package/os/rfc/handlers.ts +19 -0
  110. package/os/rfc/index.ts +97 -0
  111. package/os/rfc/rfc.module.ts +416 -0
  112. package/os/rfc/types.ts +596 -0
  113. package/os/rfc/verification-evidence.ts +244 -0
  114. package/os/session/atif-parser.ts +166 -0
  115. package/os/session/frontmatter-io.ts +163 -0
  116. package/os/session/handlers/archive.ts +288 -0
  117. package/os/session/handlers/list.ts +174 -0
  118. package/os/session/handlers/save.ts +348 -0
  119. package/os/session/handlers/validate.ts +250 -0
  120. package/os/session/index.ts +50 -0
  121. package/os/session/session.module.ts +126 -0
  122. package/os/session/types.ts +203 -0
  123. package/os/spec/live-spec-list-show-validate.test.ts +257 -0
  124. package/os/spec/live-spec-list.ts +78 -0
  125. package/os/spec/live-spec-merge.test.ts +261 -0
  126. package/os/spec/live-spec-merge.ts +413 -0
  127. package/os/spec/live-spec-show.ts +117 -0
  128. package/os/spec/live-spec-types.ts +93 -0
  129. package/os/spec/live-spec-validate.ts +170 -0
  130. package/os/spec/spec-materialize.ts +390 -0
  131. package/os/spec/spec-schema.ts +174 -0
  132. package/os/spec/spec-status.ts +284 -0
  133. package/os/spec/spec-validate.ts +513 -0
  134. package/os/spec/spec.module.ts +135 -0
  135. package/os/werkstatt/handlers/lock.ts +168 -0
  136. package/os/werkstatt/handlers/schema.ts +47 -0
  137. package/os/werkstatt/handlers/werkstatt-lock-recover.ts +175 -0
  138. package/os/werkstatt/handlers/werkstatt-lock-status.ts +73 -0
  139. package/os/werkstatt/handlers/werkstatt-operation-validate.ts +104 -0
  140. package/os/werkstatt/index.ts +39 -0
  141. package/os/werkstatt/werkstatt.module.ts +62 -0
  142. package/os/workflow/handlers.ts +304 -0
  143. package/os/workflow/index.ts +28 -0
  144. package/os/workflow/types.ts +99 -0
  145. package/os/workflow/workflow.module.ts +54 -0
  146. package/package.json +3 -3
  147. package/profiles/godot-csharp.yaml +4 -4
  148. package/profiles/knowledge-typescript-turborepo.yaml +110 -0
  149. package/profiles/phaser-turborepo.yaml +5 -5
  150. package/skills/_shared/fo-session-summary.md +132 -16
  151. package/skills/fo/fo-add-tests/pbt-guide.md +13 -13
  152. package/skills/fo/fo-doc-audit/SKILL.md +6 -6
  153. package/skills/fo/fo-handoff/SKILL.md +5 -0
  154. package/skills/fo/fo-idea/SKILL.md +1 -1
  155. package/skills/fo/fo-review/SKILL.md +1 -1
  156. package/skills/fo/fo-session-retro/SKILL.md +8 -0
  157. package/skills/fo/fo-session-save/SKILL.md +8 -0
  158. package/src/cli-output.ts +59 -0
  159. package/src/config/__tests__/resolve-terminology.test.ts +45 -0
  160. package/src/config/forge-config.ts +524 -0
  161. package/src/forge-module.ts +39 -0
  162. package/src/index.ts +172 -0
  163. package/src/knowledge/__tests__/promote.test.ts +219 -0
  164. package/src/knowledge/__tests__/serialize.test.ts +192 -0
  165. package/src/knowledge/budgets.ts +202 -0
  166. package/src/knowledge/compact.ts +405 -0
  167. package/src/knowledge/index.ts +53 -0
  168. package/src/knowledge/parse.ts +227 -0
  169. package/src/knowledge/promote.ts +156 -0
  170. package/src/knowledge/schema.ts +112 -0
  171. package/src/knowledge/serialize.ts +72 -0
  172. package/src/migration-adapters/__tests__/ignored-files.test.ts +51 -0
  173. package/src/migration-adapters/__tests__/registry.test.ts +131 -0
  174. package/src/migration-adapters/__tests__/types.test.ts +86 -0
  175. package/src/migration-adapters/git-utils.ts +71 -0
  176. package/src/migration-adapters/ignored-files.ts +221 -0
  177. package/src/migration-adapters/index.ts +21 -0
  178. package/src/migration-adapters/node-typescript-pnpm/index.ts +189 -0
  179. package/src/migration-adapters/phaser-pnpm/index.ts +189 -0
  180. package/src/migration-adapters/registry.ts +61 -0
  181. package/src/migration-adapters/types.ts +68 -0
  182. package/src/onboarding/__tests__/workspace-discovery.test.ts +134 -0
  183. package/src/onboarding/agents-generate.ts +443 -0
  184. package/src/onboarding/create.ts +427 -0
  185. package/src/onboarding/doctor.ts +1297 -0
  186. package/src/onboarding/init.ts +337 -0
  187. package/src/onboarding/invariant-engine.ts +448 -0
  188. package/src/onboarding/memory-scaffold.ts +189 -0
  189. package/src/onboarding/nested-agents-generate.ts +93 -0
  190. package/src/onboarding/nested-agents-templates.ts +233 -0
  191. package/src/onboarding/profile-validate.ts +139 -0
  192. package/src/onboarding/scaffold-project.ts +299 -0
  193. package/src/onboarding/scaffold.ts +143 -0
  194. package/src/onboarding/upgrade.ts +558 -0
  195. package/src/onboarding/workspace-discovery.ts +167 -0
  196. package/src/profiles/__tests__/profile-schema.test.ts +228 -0
  197. package/src/profiles/__tests__/stack-profile.test.ts +190 -0
  198. package/src/profiles/__tests__/terminology-utils.test.ts +71 -0
  199. package/src/profiles/profile-schema.ts +350 -0
  200. package/src/profiles/stack-profile.ts +188 -0
  201. package/src/profiles/terminology-utils.ts +36 -0
  202. package/src/registry.ts +185 -0
  203. package/src/skill-schema.ts +37 -0
  204. package/src/tests/acceptance-criteria.test.ts +99 -0
  205. package/src/tests/adr-implement-stamp.test.ts +368 -0
  206. package/src/tests/agents-generate-domain.test.ts +317 -0
  207. package/src/tests/agents-generate.test.ts +458 -0
  208. package/src/tests/bindings-schema.test.ts +314 -0
  209. package/src/tests/budgets.test.ts +213 -0
  210. package/src/tests/cli-output.test.ts +196 -0
  211. package/src/tests/compact.test.ts +796 -0
  212. package/src/tests/create.test.ts +289 -0
  213. package/src/tests/doctor-autonomy.test.ts +98 -0
  214. package/src/tests/doctor-bindings.test.ts +286 -0
  215. package/src/tests/doctor-domain.test.ts +113 -0
  216. package/src/tests/exploration-handlers.test.ts +256 -0
  217. package/src/tests/fixtures/agents-generate-business-before.txt +288 -0
  218. package/src/tests/forge-config.test.ts +420 -0
  219. package/src/tests/fs-atomic.test.ts +96 -0
  220. package/src/tests/fs.test.ts +103 -0
  221. package/src/tests/generated-marker.test.ts +161 -0
  222. package/src/tests/hash.test.ts +37 -0
  223. package/src/tests/implement-stamp.test.ts +505 -0
  224. package/src/tests/init-bindings.test.ts +183 -0
  225. package/src/tests/knowledge-parse.test.ts +307 -0
  226. package/src/tests/knowledge-pbt.test.ts +164 -0
  227. package/src/tests/memory-scaffold.test.ts +192 -0
  228. package/src/tests/migration-adapters.test.ts +480 -0
  229. package/src/tests/package-files.test.ts +54 -0
  230. package/src/tests/pinned-check.test.ts +194 -0
  231. package/src/tests/pinned-init.test.ts +126 -0
  232. package/src/tests/profile-schema.test.ts +194 -0
  233. package/src/tests/profile-validate.test.ts +120 -0
  234. package/src/tests/program-lease.test.ts +245 -0
  235. package/src/tests/program-paths-extended.test.ts +117 -0
  236. package/src/tests/program-paths.test.ts +98 -0
  237. package/src/tests/program-property.test.ts +139 -0
  238. package/src/tests/program-schemas.test.ts +445 -0
  239. package/src/tests/program-spec-node.test.ts +250 -0
  240. package/src/tests/promote.test.ts +481 -0
  241. package/src/tests/registry.test.ts +73 -0
  242. package/src/tests/scaffold-project.test.ts +150 -0
  243. package/src/tests/session-handlers.test.ts +503 -0
  244. package/src/tests/session-pbt.test.ts +190 -0
  245. package/src/tests/skill-schema.test.ts +146 -0
  246. package/src/tests/skill-validate-knowledge.test.ts +26 -0
  247. package/src/tests/skill-validate.test.ts +254 -0
  248. package/src/tests/stack-profile.test.ts +221 -0
  249. package/src/tests/string-utils.test.ts +42 -0
  250. package/src/tests/upgrade.test.ts +428 -0
  251. package/src/tests/werkstatt-lock.test.ts +343 -0
  252. package/src/tests/workspace-discovery-domain.test.ts +112 -0
  253. package/src/tests/workspace-discovery.test.ts +135 -0
  254. package/src/types.ts +221 -0
  255. package/src/utils/__tests__/fs-idempotent.test.ts +73 -0
  256. package/src/utils/__tests__/fs-trash.test.ts +45 -0
  257. package/src/utils/fs-atomic.ts +93 -0
  258. package/src/utils/fs-idempotent.ts +41 -0
  259. package/src/utils/fs-trash-sync.ts +37 -0
  260. package/src/utils/fs-trash.ts +24 -0
  261. package/src/utils/fs.ts +74 -0
  262. package/src/utils/generated-marker.ts +166 -0
  263. package/src/utils/hash.ts +19 -0
  264. package/src/utils/index.ts +29 -0
  265. package/src/utils/string-utils.ts +19 -0
  266. package/src/validators/__tests__/note-orphan-detect.test.ts +94 -0
  267. package/src/validators/note-frontmatter-validate.ts +142 -0
  268. package/src/validators/note-link-validate.ts +145 -0
  269. package/src/validators/note-orphan-detect.ts +146 -0
  270. package/src/validators/port-validate.ts +97 -0
  271. package/src/validators/skill-validate.ts +831 -0
@@ -0,0 +1,796 @@
1
+ import { test, expect, describe } from "vitest";
2
+ import {
3
+ planCompaction,
4
+ executeCompaction,
5
+ resolveRetentionDays,
6
+ resolveStaleDays,
7
+ DEFAULT_RETENTION_DAYS,
8
+ DEFAULT_STALE_DAYS,
9
+ type CompactOptions,
10
+ } from "../knowledge/compact.ts";
11
+ import { parseKnowledgeFile } from "../knowledge/parse.ts";
12
+ import type {
13
+ ParsedKnowledgeFile,
14
+ KnowledgeLayer,
15
+ KnowledgeEntryMeta,
16
+ } from "../knowledge/schema.ts";
17
+ import fs from "node:fs";
18
+ import path from "node:path";
19
+ import os from "node:os";
20
+
21
+ const testDir = path.join(os.tmpdir(), "test");
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Helpers
25
+ // ---------------------------------------------------------------------------
26
+
27
+ function makeMeta(overrides: Partial<KnowledgeEntryMeta> = {}): KnowledgeEntryMeta {
28
+ const layer = overrides.layer ?? "L2";
29
+ const base: KnowledgeEntryMeta = {
30
+ id: "K-0001",
31
+ layer,
32
+ created: "2026-01-01",
33
+ status: "active",
34
+ };
35
+ // L2 requires confirmations and lastConfirmedAt per schema
36
+ if (layer === "L2") {
37
+ base.confirmations = 1;
38
+ base.lastConfirmedAt = "2026-01-01";
39
+ }
40
+ return { ...base, ...overrides };
41
+ }
42
+
43
+ function makeParsedFile(overrides: Partial<ParsedKnowledgeFile> = {}): ParsedKnowledgeFile {
44
+ return {
45
+ path: path.join(testDir, "learned-principles.md"),
46
+ layer: "L2",
47
+ preamble: "",
48
+ entries: [],
49
+ legacySections: [],
50
+ parseIssues: [],
51
+ isKnowledgeAdjacent: false,
52
+ ...overrides,
53
+ };
54
+ }
55
+
56
+ function makeEntry(
57
+ id: string,
58
+ meta: Partial<KnowledgeEntryMeta>,
59
+ body = "Body text.",
60
+ ): {
61
+ meta: KnowledgeEntryMeta;
62
+ title: string;
63
+ body: string;
64
+ lineStart: number;
65
+ } {
66
+ return {
67
+ meta: makeMeta({ id, ...meta }),
68
+ title: `Entry ${id}`,
69
+ body,
70
+ lineStart: 1,
71
+ };
72
+ }
73
+
74
+ function writeTempKnowledgeFile(
75
+ content: string,
76
+ fileName: string,
77
+ ): { filePath: string; dir: string } {
78
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "forge-compact-test-"));
79
+ const filePath = path.join(dir, fileName);
80
+ fs.writeFileSync(filePath, content, "utf8");
81
+ return { filePath, dir };
82
+ }
83
+
84
+ function buildKnowledgeMarkdown(
85
+ layer: KnowledgeLayer,
86
+ entries: KnowledgeEntryMeta[],
87
+ fileName: string,
88
+ ): string {
89
+ const parts: string[] = [`<!-- knowledge-layer: ${layer} -->`, `# ${fileName}`, ""];
90
+ for (const meta of entries) {
91
+ parts.push(`### ${meta.id}: Test entry`);
92
+ parts.push("");
93
+ parts.push("```knowledge-entry");
94
+ parts.push(`id: ${meta.id}`);
95
+ parts.push(`layer: ${meta.layer}`);
96
+ parts.push(`created: ${meta.created}`);
97
+ if (meta.lastConfirmedAt !== undefined)
98
+ parts.push(`lastConfirmedAt: ${meta.lastConfirmedAt ?? "null"}`);
99
+ if (meta.confirmations !== undefined) parts.push(`confirmations: ${meta.confirmations}`);
100
+ if (meta.expiresAt !== undefined) parts.push(`expiresAt: ${meta.expiresAt ?? "null"}`);
101
+ if (meta.supersedes !== undefined) parts.push(`supersedes: [${meta.supersedes.join(", ")}]`);
102
+ if (meta.promotedTo !== undefined) parts.push(`promotedTo: ${meta.promotedTo ?? "null"}`);
103
+ parts.push(`status: ${meta.status}`);
104
+ parts.push("```");
105
+ parts.push("");
106
+ parts.push("Body text for this entry.");
107
+ parts.push("");
108
+ }
109
+ return parts.join("\n");
110
+ }
111
+
112
+ const TODAY = "2026-08-03";
113
+ const DEFAULT_OPTIONS: CompactOptions = {
114
+ retentionDays: 90,
115
+ staleDays: 90,
116
+ today: TODAY,
117
+ };
118
+
119
+ // ---------------------------------------------------------------------------
120
+ // planCompaction tests
121
+ // ---------------------------------------------------------------------------
122
+
123
+ describe("planCompaction", () => {
124
+ test("returns empty plan for file with no entries", () => {
125
+ const file = makeParsedFile({ entries: [] });
126
+ const plans = planCompaction([file], DEFAULT_OPTIONS);
127
+ expect(plans).toHaveLength(1);
128
+ expect(plans[0].actions).toHaveLength(0);
129
+ expect(plans[0].legacySectionCount).toBe(0);
130
+ });
131
+
132
+ test("skips knowledge-adjacent files", () => {
133
+ const file = makeParsedFile({ isKnowledgeAdjacent: true });
134
+ const plans = planCompaction([file], DEFAULT_OPTIONS);
135
+ expect(plans).toHaveLength(0);
136
+ });
137
+
138
+ test("archive-expired: entry with expiresAt in the past", () => {
139
+ const entry = makeEntry("K-0001", {
140
+ layer: "L2",
141
+ created: "2026-01-01",
142
+ expiresAt: "2026-07-01",
143
+ status: "active",
144
+ });
145
+ const file = makeParsedFile({ entries: [entry] });
146
+ const plans = planCompaction([file], DEFAULT_OPTIONS);
147
+ expect(plans[0].actions).toHaveLength(1);
148
+ expect(plans[0].actions[0].kind).toBe("archive-expired");
149
+ expect(plans[0].actions[0].entryId).toBe("K-0001");
150
+ expect(plans[0].actions[0].reason).toContain("expiresAt 2026-07-01");
151
+ });
152
+
153
+ test("does NOT archive-expired when expiresAt is today or future", () => {
154
+ const entryToday = makeEntry("K-0001", {
155
+ layer: "L2",
156
+ created: "2026-07-01",
157
+ lastConfirmedAt: "2026-07-15",
158
+ confirmations: 1,
159
+ expiresAt: TODAY,
160
+ status: "active",
161
+ });
162
+ const entryFuture = makeEntry("K-0002", {
163
+ layer: "L2",
164
+ created: "2026-07-01",
165
+ lastConfirmedAt: "2026-07-15",
166
+ confirmations: 1,
167
+ expiresAt: "2026-12-31",
168
+ status: "active",
169
+ });
170
+ const file = makeParsedFile({ entries: [entryToday, entryFuture] });
171
+ const plans = planCompaction([file], DEFAULT_OPTIONS);
172
+ expect(plans[0].actions).toHaveLength(0);
173
+ });
174
+
175
+ test("archive-superseded: entry with status superseded", () => {
176
+ const entry = makeEntry("K-0001", {
177
+ layer: "L2",
178
+ created: "2026-01-01",
179
+ status: "superseded",
180
+ });
181
+ const file = makeParsedFile({ entries: [entry] });
182
+ const plans = planCompaction([file], DEFAULT_OPTIONS);
183
+ expect(plans[0].actions).toHaveLength(1);
184
+ expect(plans[0].actions[0].kind).toBe("archive-superseded");
185
+ });
186
+
187
+ test("archive-l0-retention: L0 entry older than retentionDays", () => {
188
+ const entry = makeEntry("K-0001", {
189
+ layer: "L0",
190
+ created: "2026-01-01",
191
+ status: "active",
192
+ });
193
+ const file = makeParsedFile({ layer: "L0", entries: [entry] });
194
+ const plans = planCompaction([file], DEFAULT_OPTIONS);
195
+ expect(plans[0].actions).toHaveLength(1);
196
+ expect(plans[0].actions[0].kind).toBe("archive-l0-retention");
197
+ expect(plans[0].actions[0].reason).toContain("older than 90 days");
198
+ });
199
+
200
+ test("does NOT archive-l0-retention when L0 entry is within retention", () => {
201
+ const entry = makeEntry("K-0001", {
202
+ layer: "L0",
203
+ created: "2026-07-01",
204
+ status: "active",
205
+ });
206
+ const file = makeParsedFile({ layer: "L0", entries: [entry] });
207
+ const plans = planCompaction([file], DEFAULT_OPTIONS);
208
+ expect(plans[0].actions).toHaveLength(0);
209
+ });
210
+
211
+ test("mark-stale: L2 active entry with old lastConfirmedAt", () => {
212
+ const entry = makeEntry("K-0001", {
213
+ layer: "L2",
214
+ created: "2026-01-01",
215
+ lastConfirmedAt: "2026-01-15",
216
+ confirmations: 3,
217
+ status: "active",
218
+ });
219
+ const file = makeParsedFile({ layer: "L2", entries: [entry] });
220
+ const plans = planCompaction([file], DEFAULT_OPTIONS);
221
+ expect(plans[0].actions).toHaveLength(1);
222
+ expect(plans[0].actions[0].kind).toBe("mark-stale");
223
+ expect(plans[0].actions[0].reason).toContain("lastConfirmedAt 2026-01-15");
224
+ });
225
+
226
+ test("does NOT mark-stale when lastConfirmedAt is within staleDays", () => {
227
+ const entry = makeEntry("K-0001", {
228
+ layer: "L2",
229
+ created: "2026-01-01",
230
+ lastConfirmedAt: "2026-07-01",
231
+ confirmations: 3,
232
+ status: "active",
233
+ });
234
+ const file = makeParsedFile({ layer: "L2", entries: [entry] });
235
+ const plans = planCompaction([file], DEFAULT_OPTIONS);
236
+ expect(plans[0].actions).toHaveLength(0);
237
+ });
238
+
239
+ test("does NOT mark-stale for L1 entries (only L2)", () => {
240
+ const entry = makeEntry("K-0001", {
241
+ layer: "L1",
242
+ created: "2026-01-01",
243
+ status: "active",
244
+ });
245
+ const file = makeParsedFile({ layer: "L1", entries: [entry] });
246
+ const plans = planCompaction([file], DEFAULT_OPTIONS);
247
+ expect(plans[0].actions).toHaveLength(0);
248
+ });
249
+
250
+ test("does NOT mark-stale when status is already stale", () => {
251
+ const entry = makeEntry("K-0001", {
252
+ layer: "L2",
253
+ created: "2026-01-01",
254
+ lastConfirmedAt: "2026-01-15",
255
+ confirmations: 3,
256
+ status: "stale",
257
+ });
258
+ const file = makeParsedFile({ layer: "L2", entries: [entry] });
259
+ const plans = planCompaction([file], DEFAULT_OPTIONS);
260
+ expect(plans[0].actions).toHaveLength(0);
261
+ });
262
+
263
+ test("reports legacy section count", () => {
264
+ const file = makeParsedFile({
265
+ legacySections: [
266
+ { text: "## Old section\n\nSome prose.", lineStart: 10 },
267
+ { text: "## Another old section\n\nMore prose.", lineStart: 20 },
268
+ ],
269
+ });
270
+ const plans = planCompaction([file], DEFAULT_OPTIONS);
271
+ expect(plans[0].legacySectionCount).toBe(2);
272
+ });
273
+
274
+ test("respects custom retentionDays", () => {
275
+ const entry = makeEntry("K-0001", {
276
+ layer: "L0",
277
+ created: "2026-07-01",
278
+ status: "active",
279
+ });
280
+ const file = makeParsedFile({ layer: "L0", entries: [entry] });
281
+ const plans = planCompaction([file], { ...DEFAULT_OPTIONS, retentionDays: 10 });
282
+ expect(plans[0].actions).toHaveLength(1);
283
+ expect(plans[0].actions[0].kind).toBe("archive-l0-retention");
284
+ });
285
+
286
+ test("respects custom staleDays", () => {
287
+ const entry = makeEntry("K-0001", {
288
+ layer: "L2",
289
+ created: "2026-01-01",
290
+ lastConfirmedAt: "2026-07-15",
291
+ confirmations: 3,
292
+ status: "active",
293
+ });
294
+ const file = makeParsedFile({ layer: "L2", entries: [entry] });
295
+ const plans = planCompaction([file], { ...DEFAULT_OPTIONS, staleDays: 10 });
296
+ expect(plans[0].actions).toHaveLength(1);
297
+ expect(plans[0].actions[0].kind).toBe("mark-stale");
298
+ });
299
+
300
+ test("archiveFile path is correct for qa-log.md", () => {
301
+ const file = makeParsedFile({ path: path.join(testDir, "qa-log.md"), layer: "L0" });
302
+ const plans = planCompaction([file], DEFAULT_OPTIONS);
303
+ expect(plans[0].archiveFile).toBe(path.join(testDir, "qa-log.archive.md"));
304
+ });
305
+
306
+ test("archiveFile path is correct for learned-principles.md", () => {
307
+ const file = makeParsedFile({ path: path.join(testDir, "learned-principles.md"), layer: "L2" });
308
+ const plans = planCompaction([file], DEFAULT_OPTIONS);
309
+ expect(plans[0].archiveFile).toBe(path.join(testDir, "learned-principles.archive.md"));
310
+ });
311
+ });
312
+
313
+ // ---------------------------------------------------------------------------
314
+ // executeCompaction tests
315
+ // ---------------------------------------------------------------------------
316
+
317
+ describe("executeCompaction", () => {
318
+ test("dry-run produces no writes", () => {
319
+ const { filePath, dir } = writeTempKnowledgeFile(
320
+ buildKnowledgeMarkdown(
321
+ "L2",
322
+ [
323
+ makeMeta({
324
+ id: "K-0001",
325
+ layer: "L2",
326
+ created: "2026-01-01",
327
+ lastConfirmedAt: "2026-01-15",
328
+ confirmations: 3,
329
+ status: "active",
330
+ }),
331
+ ],
332
+ "learned-principles.md",
333
+ ),
334
+ "learned-principles.md",
335
+ );
336
+
337
+ try {
338
+ const parsed = parseKnowledgeFile(filePath);
339
+ const plans = planCompaction([parsed], DEFAULT_OPTIONS);
340
+ const report = executeCompaction(plans, true); // dry-run
341
+
342
+ expect(report.status).toBe("pass");
343
+ expect(report.dryRun).toBe(true);
344
+ expect(report.totals.markedStale).toBe(1);
345
+ expect(report.files[0].written).toBe(false);
346
+
347
+ // File should be unchanged
348
+ const content = fs.readFileSync(filePath, "utf8");
349
+ expect(content).toContain("status: active");
350
+ } finally {
351
+ fs.rmSync(dir, { recursive: true, force: true });
352
+ }
353
+ });
354
+
355
+ test("mark-stale writes updated status to live file", () => {
356
+ const { filePath, dir } = writeTempKnowledgeFile(
357
+ buildKnowledgeMarkdown(
358
+ "L2",
359
+ [
360
+ makeMeta({
361
+ id: "K-0001",
362
+ layer: "L2",
363
+ created: "2026-01-01",
364
+ lastConfirmedAt: "2026-01-15",
365
+ confirmations: 3,
366
+ status: "active",
367
+ }),
368
+ ],
369
+ "learned-principles.md",
370
+ ),
371
+ "learned-principles.md",
372
+ );
373
+
374
+ try {
375
+ const parsed = parseKnowledgeFile(filePath);
376
+ const plans = planCompaction([parsed], DEFAULT_OPTIONS);
377
+ const report = executeCompaction(plans, false); // live run
378
+
379
+ expect(report.status).toBe("pass");
380
+ expect(report.totals.markedStale).toBe(1);
381
+
382
+ const content = fs.readFileSync(filePath, "utf8");
383
+ expect(content).toContain("status: stale");
384
+ expect(content).not.toContain("status: active");
385
+ } finally {
386
+ fs.rmSync(dir, { recursive: true, force: true });
387
+ }
388
+ });
389
+
390
+ test("archive-expired moves entry to archive companion", () => {
391
+ const { filePath, dir } = writeTempKnowledgeFile(
392
+ buildKnowledgeMarkdown(
393
+ "L2",
394
+ [
395
+ makeMeta({
396
+ id: "K-0001",
397
+ layer: "L2",
398
+ created: "2026-01-01",
399
+ lastConfirmedAt: "2026-01-15",
400
+ confirmations: 3,
401
+ expiresAt: "2026-07-01",
402
+ status: "active",
403
+ }),
404
+ makeMeta({
405
+ id: "K-0002",
406
+ layer: "L2",
407
+ created: "2026-07-01",
408
+ lastConfirmedAt: "2026-07-15",
409
+ confirmations: 1,
410
+ status: "active",
411
+ }),
412
+ ],
413
+ "learned-principles.md",
414
+ ),
415
+ "learned-principles.md",
416
+ );
417
+
418
+ try {
419
+ const parsed = parseKnowledgeFile(filePath);
420
+ const plans = planCompaction([parsed], DEFAULT_OPTIONS);
421
+ const report = executeCompaction(plans, false); // live run
422
+
423
+ expect(report.status).toBe("pass");
424
+ expect(report.totals.archived).toBe(1);
425
+
426
+ // Live file should only have K-0002
427
+ const liveContent = fs.readFileSync(filePath, "utf8");
428
+ expect(liveContent).not.toContain("K-0001");
429
+ expect(liveContent).toContain("K-0002");
430
+
431
+ // Archive file should have K-0001 with status: archived
432
+ const archivePath = path.join(dir, "learned-principles.archive.md");
433
+ expect(fs.existsSync(archivePath)).toBe(true);
434
+ const archiveContent = fs.readFileSync(archivePath, "utf8");
435
+ expect(archiveContent).toContain("K-0001");
436
+ expect(archiveContent).toContain("status: archived");
437
+ } finally {
438
+ fs.rmSync(dir, { recursive: true, force: true });
439
+ }
440
+ });
441
+
442
+ test("archive-superseded preserves status as superseded in archive", () => {
443
+ const { filePath, dir } = writeTempKnowledgeFile(
444
+ buildKnowledgeMarkdown(
445
+ "L2",
446
+ [
447
+ makeMeta({
448
+ id: "K-0001",
449
+ layer: "L2",
450
+ created: "2026-01-01",
451
+ lastConfirmedAt: "2026-01-15",
452
+ confirmations: 3,
453
+ status: "superseded",
454
+ }),
455
+ makeMeta({
456
+ id: "K-0002",
457
+ layer: "L2",
458
+ created: "2026-02-01",
459
+ lastConfirmedAt: "2026-07-15",
460
+ confirmations: 1,
461
+ status: "active",
462
+ }),
463
+ ],
464
+ "learned-principles.md",
465
+ ),
466
+ "learned-principles.md",
467
+ );
468
+
469
+ try {
470
+ const parsed = parseKnowledgeFile(filePath);
471
+ const plans = planCompaction([parsed], DEFAULT_OPTIONS);
472
+ const report = executeCompaction(plans, false);
473
+
474
+ expect(report.totals.archived).toBe(1);
475
+
476
+ const archivePath = path.join(dir, "learned-principles.archive.md");
477
+ const archiveContent = fs.readFileSync(archivePath, "utf8");
478
+ expect(archiveContent).toContain("K-0001");
479
+ expect(archiveContent).toContain("status: superseded");
480
+ } finally {
481
+ fs.rmSync(dir, { recursive: true, force: true });
482
+ }
483
+ });
484
+
485
+ test("archive-l0-retention moves L0 entries to qa-log.archive.md", () => {
486
+ const { filePath, dir } = writeTempKnowledgeFile(
487
+ buildKnowledgeMarkdown(
488
+ "L0",
489
+ [
490
+ makeMeta({
491
+ id: "K-0001",
492
+ layer: "L0",
493
+ created: "2026-01-01",
494
+ status: "active",
495
+ }),
496
+ ],
497
+ "qa-log.md",
498
+ ),
499
+ "qa-log.md",
500
+ );
501
+
502
+ try {
503
+ const parsed = parseKnowledgeFile(filePath);
504
+ const plans = planCompaction([parsed], DEFAULT_OPTIONS);
505
+ const report = executeCompaction(plans, false);
506
+
507
+ expect(report.totals.archived).toBe(1);
508
+
509
+ const archivePath = path.join(dir, "qa-log.archive.md");
510
+ expect(fs.existsSync(archivePath)).toBe(true);
511
+ const archiveContent = fs.readFileSync(archivePath, "utf8");
512
+ expect(archiveContent).toContain("K-0001");
513
+ expect(archiveContent).toContain("status: archived");
514
+ } finally {
515
+ fs.rmSync(dir, { recursive: true, force: true });
516
+ }
517
+ });
518
+
519
+ test("append-merge: existing archive companion receives new entries", () => {
520
+ const { filePath, dir } = writeTempKnowledgeFile(
521
+ buildKnowledgeMarkdown(
522
+ "L2",
523
+ [
524
+ makeMeta({
525
+ id: "K-0003",
526
+ layer: "L2",
527
+ created: "2026-01-01",
528
+ lastConfirmedAt: "2026-01-15",
529
+ confirmations: 3,
530
+ expiresAt: "2026-07-01",
531
+ status: "active",
532
+ }),
533
+ ],
534
+ "learned-principles.md",
535
+ ),
536
+ "learned-principles.md",
537
+ );
538
+
539
+ // Pre-create archive companion with an existing entry
540
+ const archivePath = path.join(dir, "learned-principles.archive.md");
541
+ fs.writeFileSync(
542
+ archivePath,
543
+ buildKnowledgeMarkdown(
544
+ "L2",
545
+ [
546
+ makeMeta({
547
+ id: "K-0001",
548
+ layer: "L2",
549
+ created: "2025-01-01",
550
+ lastConfirmedAt: "2025-01-15",
551
+ confirmations: 5,
552
+ status: "archived",
553
+ }),
554
+ ],
555
+ "learned-principles.archive.md",
556
+ ),
557
+ "utf8",
558
+ );
559
+
560
+ try {
561
+ const parsed = parseKnowledgeFile(filePath);
562
+ const plans = planCompaction([parsed], DEFAULT_OPTIONS);
563
+ const report = executeCompaction(plans, false);
564
+
565
+ expect(report.totals.archived).toBe(1);
566
+
567
+ const archiveContent = fs.readFileSync(archivePath, "utf8");
568
+ // Both old and new entries should be present
569
+ expect(archiveContent).toContain("K-0001");
570
+ expect(archiveContent).toContain("K-0003");
571
+ } finally {
572
+ fs.rmSync(dir, { recursive: true, force: true });
573
+ }
574
+ });
575
+
576
+ test("refuses to compact file with parse issues", () => {
577
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "forge-compact-test-"));
578
+ const filePath = path.join(dir, "learned-principles.md");
579
+ // Write a file with a bad metadata block to trigger parse issues
580
+ fs.writeFileSync(
581
+ filePath,
582
+ "<!-- knowledge-layer: L2 -->\n# learned-principles.md\n\n### K-0001: Test\n\n```knowledge-entry\nid: K-0001\nlayer: INVALID\ncreated: not-a-date\nstatus: active\n```\n\nBody.\n",
583
+ "utf8",
584
+ );
585
+
586
+ try {
587
+ const parsed = parseKnowledgeFile(filePath);
588
+ // If the parser produces parse issues, executeCompaction should refuse
589
+ if (parsed.parseIssues.length > 0) {
590
+ const plans = planCompaction([parsed], DEFAULT_OPTIONS);
591
+ const report = executeCompaction(plans, false);
592
+
593
+ expect(report.status).toBe("fail");
594
+ expect(report.errors).toBeDefined();
595
+ expect(report.errors!.length).toBeGreaterThan(0);
596
+ expect(report.files[0].written).toBe(false);
597
+ }
598
+ } finally {
599
+ fs.rmSync(dir, { recursive: true, force: true });
600
+ }
601
+ });
602
+
603
+ test("no actions on a file produces no writes", () => {
604
+ const { filePath, dir } = writeTempKnowledgeFile(
605
+ buildKnowledgeMarkdown(
606
+ "L2",
607
+ [
608
+ makeMeta({
609
+ id: "K-0001",
610
+ layer: "L2",
611
+ created: "2026-07-01",
612
+ lastConfirmedAt: "2026-07-15",
613
+ confirmations: 1,
614
+ status: "active",
615
+ }),
616
+ ],
617
+ "learned-principles.md",
618
+ ),
619
+ "learned-principles.md",
620
+ );
621
+
622
+ try {
623
+ const originalContent = fs.readFileSync(filePath, "utf8");
624
+ const parsed = parseKnowledgeFile(filePath);
625
+ const plans = planCompaction([parsed], DEFAULT_OPTIONS);
626
+ expect(plans[0].actions).toHaveLength(0);
627
+
628
+ const report = executeCompaction(plans, false);
629
+ expect(report.status).toBe("pass");
630
+ expect(report.totals.archived).toBe(0);
631
+ expect(report.totals.markedStale).toBe(0);
632
+
633
+ // File should be unchanged
634
+ const content = fs.readFileSync(filePath, "utf8");
635
+ expect(content).toBe(originalContent);
636
+ } finally {
637
+ fs.rmSync(dir, { recursive: true, force: true });
638
+ }
639
+ });
640
+ });
641
+
642
+ // ---------------------------------------------------------------------------
643
+ // Round-trip property: active, non-expired entries are byte-identical after a run
644
+ // ---------------------------------------------------------------------------
645
+
646
+ describe("Round-trip: active entries preserved", () => {
647
+ test("active entry body is preserved after mark-stale on another entry", () => {
648
+ const { filePath, dir } = writeTempKnowledgeFile(
649
+ buildKnowledgeMarkdown(
650
+ "L2",
651
+ [
652
+ makeMeta({
653
+ id: "K-0001",
654
+ layer: "L2",
655
+ created: "2026-01-01",
656
+ lastConfirmedAt: "2026-07-15",
657
+ confirmations: 1,
658
+ status: "active",
659
+ }),
660
+ makeMeta({
661
+ id: "K-0002",
662
+ layer: "L2",
663
+ created: "2026-01-01",
664
+ lastConfirmedAt: "2026-01-15",
665
+ confirmations: 2,
666
+ status: "active",
667
+ }),
668
+ ],
669
+ "learned-principles.md",
670
+ ),
671
+ "learned-principles.md",
672
+ );
673
+
674
+ try {
675
+ const parsed = parseKnowledgeFile(filePath);
676
+ const plans = planCompaction([parsed], DEFAULT_OPTIONS);
677
+
678
+ // K-0002 should be mark-stale, K-0001 should have no action
679
+ const k1Action = plans[0].actions.find((a) => a.entryId === "K-0001");
680
+ const k2Action = plans[0].actions.find((a) => a.entryId === "K-0002");
681
+ expect(k1Action).toBeUndefined();
682
+ expect(k2Action?.kind).toBe("mark-stale");
683
+
684
+ executeCompaction(plans, false);
685
+
686
+ // Re-parse and check K-0001 is still active, K-0002 is stale
687
+ const reparsed = parseKnowledgeFile(filePath);
688
+ const k1 = reparsed.entries.find((e) => e.meta.id === "K-0001");
689
+ const k2 = reparsed.entries.find((e) => e.meta.id === "K-0002");
690
+ expect(k1?.meta.status).toBe("active");
691
+ expect(k2?.meta.status).toBe("stale");
692
+ // K-0001 body should be preserved
693
+ expect(k1?.body).toContain("Body text for this entry.");
694
+ } finally {
695
+ fs.rmSync(dir, { recursive: true, force: true });
696
+ }
697
+ });
698
+ });
699
+
700
+ // ---------------------------------------------------------------------------
701
+ // resolveRetentionDays / resolveStaleDays tests
702
+ // ---------------------------------------------------------------------------
703
+
704
+ describe("resolveRetentionDays", () => {
705
+ test("returns default when forge.yaml not found", () => {
706
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "forge-compact-cfg-"));
707
+ try {
708
+ const days = resolveRetentionDays(tmpDir);
709
+ expect(days).toBe(DEFAULT_RETENTION_DAYS);
710
+ } finally {
711
+ fs.rmSync(tmpDir, { recursive: true, force: true });
712
+ }
713
+ });
714
+
715
+ test("returns default when no knowledge.retentionDays override", () => {
716
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "forge-compact-cfg-"));
717
+ try {
718
+ fs.writeFileSync(
719
+ path.join(tmpDir, "forge.yaml"),
720
+ "schema: forge/bindings@1\nbindings:\n commands:\n validateRfc: null\n",
721
+ );
722
+ const days = resolveRetentionDays(tmpDir);
723
+ expect(days).toBe(DEFAULT_RETENTION_DAYS);
724
+ } finally {
725
+ fs.rmSync(tmpDir, { recursive: true, force: true });
726
+ }
727
+ });
728
+
729
+ test("applies override when present and valid", () => {
730
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "forge-compact-cfg-"));
731
+ try {
732
+ fs.writeFileSync(
733
+ path.join(tmpDir, "forge.yaml"),
734
+ "schema: forge/bindings@1\nbindings:\n knowledge:\n retentionDays: 30\n",
735
+ );
736
+ const days = resolveRetentionDays(tmpDir);
737
+ expect(days).toBe(30);
738
+ } finally {
739
+ fs.rmSync(tmpDir, { recursive: true, force: true });
740
+ }
741
+ });
742
+
743
+ test("falls back to default when override is invalid (non-positive)", () => {
744
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "forge-compact-cfg-"));
745
+ try {
746
+ fs.writeFileSync(
747
+ path.join(tmpDir, "forge.yaml"),
748
+ "schema: forge/bindings@1\nbindings:\n knowledge:\n retentionDays: 0\n",
749
+ );
750
+ const days = resolveRetentionDays(tmpDir);
751
+ expect(days).toBe(DEFAULT_RETENTION_DAYS);
752
+ } finally {
753
+ fs.rmSync(tmpDir, { recursive: true, force: true });
754
+ }
755
+ });
756
+ });
757
+
758
+ describe("resolveStaleDays", () => {
759
+ test("returns default when forge.yaml not found", () => {
760
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "forge-compact-cfg-"));
761
+ try {
762
+ const days = resolveStaleDays(tmpDir);
763
+ expect(days).toBe(DEFAULT_STALE_DAYS);
764
+ } finally {
765
+ fs.rmSync(tmpDir, { recursive: true, force: true });
766
+ }
767
+ });
768
+
769
+ test("applies override when present and valid", () => {
770
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "forge-compact-cfg-"));
771
+ try {
772
+ fs.writeFileSync(
773
+ path.join(tmpDir, "forge.yaml"),
774
+ "schema: forge/bindings@1\nbindings:\n knowledge:\n staleDays: 45\n",
775
+ );
776
+ const days = resolveStaleDays(tmpDir);
777
+ expect(days).toBe(45);
778
+ } finally {
779
+ fs.rmSync(tmpDir, { recursive: true, force: true });
780
+ }
781
+ });
782
+
783
+ test("falls back to default when override is non-integer", () => {
784
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "forge-compact-cfg-"));
785
+ try {
786
+ fs.writeFileSync(
787
+ path.join(tmpDir, "forge.yaml"),
788
+ "schema: forge/bindings@1\nbindings:\n knowledge:\n staleDays: 3.5\n",
789
+ );
790
+ const days = resolveStaleDays(tmpDir);
791
+ expect(days).toBe(DEFAULT_STALE_DAYS);
792
+ } finally {
793
+ fs.rmSync(tmpDir, { recursive: true, force: true });
794
+ }
795
+ });
796
+ });