devflow-kit 2.3.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/CHANGELOG.md +177 -0
  2. package/README.md +86 -18
  3. package/dist/agents/git.md +824 -0
  4. package/dist/cli/commands/agents.js +6 -1
  5. package/dist/cli/commands/attribution-prompts.js +144 -0
  6. package/dist/cli/commands/compliance-prompts.js +7 -11
  7. package/dist/cli/commands/compliance.js +23 -1
  8. package/dist/cli/commands/init-seed.js +69 -30
  9. package/dist/cli/commands/init.js +551 -77
  10. package/dist/cli/commands/install-report.js +205 -0
  11. package/dist/cli/commands/knowledge/index.js +2 -2
  12. package/dist/cli/commands/knowledge/toggle.js +27 -37
  13. package/dist/cli/commands/learning.js +37 -30
  14. package/dist/cli/commands/memory.js +79 -69
  15. package/dist/cli/commands/prompt-io.js +38 -0
  16. package/dist/cli/commands/proxy.js +91 -31
  17. package/dist/cli/commands/security.js +76 -16
  18. package/dist/cli/commands/skills.js +53 -7
  19. package/dist/cli/commands/tracker-prompts.js +145 -0
  20. package/dist/cli/commands/tracker.js +405 -0
  21. package/dist/cli/commands/uninstall.js +233 -77
  22. package/dist/cli.js +2 -0
  23. package/dist/commands/bug-analysis.md +22 -4
  24. package/dist/commands/code-review.md +44 -15
  25. package/dist/commands/debug.md +20 -6
  26. package/dist/commands/dynamic-build.md +289 -67
  27. package/dist/commands/dynamic-plan.md +60 -21
  28. package/dist/commands/dynamic-profile.md +1 -1
  29. package/dist/commands/dynamic-tickets.md +58 -8
  30. package/dist/commands/explore.md +2 -2
  31. package/dist/commands/implement.md +241 -53
  32. package/dist/commands/plan.md +88 -17
  33. package/dist/commands/release.md +64 -17
  34. package/dist/commands/resolve.md +138 -58
  35. package/dist/commands/self-review.md +2 -2
  36. package/dist/core/agent-models.js +55 -12
  37. package/dist/core/assets.js +58 -2
  38. package/dist/core/evidence-policy.js +147 -0
  39. package/dist/core/feature-config.js +130 -64
  40. package/dist/core/feature-switch.js +112 -0
  41. package/dist/core/flags.js +149 -8
  42. package/dist/core/manifest.js +33 -7
  43. package/dist/core/mds-variants.js +861 -0
  44. package/dist/core/model-discovery.js +12 -1
  45. package/dist/core/plugins.js +357 -9
  46. package/dist/core/project-paths.js +1 -1
  47. package/dist/core/proxy-log.js +8 -4
  48. package/dist/core/proxy-state.js +100 -43
  49. package/dist/core/reference-sweep.js +136 -0
  50. package/dist/core/tracker.js +407 -0
  51. package/dist/skills/git/references/decision-markers.md +19 -0
  52. package/dist/skills/git/references/learn-conventions.md +56 -0
  53. package/dist/skills/git/references/pr/check-ci-status.md +14 -0
  54. package/dist/skills/git/references/pr/check-merge-readiness.md +28 -0
  55. package/dist/skills/git/references/pr/ensure-pr-ready.md +24 -0
  56. package/dist/skills/git/references/pr/fetch-review-threads.md +22 -0
  57. package/dist/skills/git/references/pr/post-resolution-summary.md +40 -0
  58. package/dist/skills/git/references/pr/post-review-summary.md +42 -0
  59. package/dist/skills/git/references/pr/resolve-review-threads.md +35 -0
  60. package/dist/skills/git/references/pr/update-pr-evidence.md +14 -0
  61. package/dist/skills/git/references/pr/validate-branch.md +18 -0
  62. package/dist/skills/git/references/publication-gate.md +13 -0
  63. package/dist/skills/git/references/tracker/_mcp.md +153 -0
  64. package/dist/skills/git/references/tracker/github/associate-release.md +18 -0
  65. package/dist/skills/git/references/tracker/github/backlink-shipped-issues.md +40 -0
  66. package/dist/skills/git/references/tracker/github/create-release.md +11 -0
  67. package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +16 -0
  68. package/dist/skills/git/references/tracker/github/ensure-traceable-issue.md +69 -0
  69. package/dist/skills/git/references/tracker/github/fetch-issue.md +32 -0
  70. package/dist/skills/git/references/tracker/github/fetch-issues-batch.md +17 -0
  71. package/dist/skills/git/references/tracker/github/gather-release-evidence.md +19 -0
  72. package/dist/skills/git/references/tracker/github/manage-debt.md +101 -0
  73. package/dist/skills/git/references/tracker/github/post-wave-report.md +28 -0
  74. package/dist/skills/git/references/tracker/github/setup-task.md +26 -0
  75. package/dist/skills/git/references/tracker/jira/associate-release.md +18 -0
  76. package/dist/skills/git/references/tracker/jira/backlink-shipped-issues.md +49 -0
  77. package/dist/skills/git/references/tracker/jira/create-release.md +17 -0
  78. package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +22 -0
  79. package/dist/skills/git/references/tracker/jira/ensure-traceable-issue.md +53 -0
  80. package/dist/skills/git/references/tracker/jira/fetch-issue.md +14 -0
  81. package/dist/skills/git/references/tracker/jira/fetch-issues-batch.md +15 -0
  82. package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +18 -0
  83. package/dist/skills/git/references/tracker/jira/manage-debt.md +37 -0
  84. package/dist/skills/git/references/tracker/jira/post-wave-report.md +33 -0
  85. package/dist/skills/git/references/tracker/jira/setup-task.md +31 -0
  86. package/dist/skills/git/references/tracker/linear/associate-release.md +18 -0
  87. package/dist/skills/git/references/tracker/linear/backlink-shipped-issues.md +53 -0
  88. package/dist/skills/git/references/tracker/linear/create-release.md +17 -0
  89. package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +22 -0
  90. package/dist/skills/git/references/tracker/linear/ensure-traceable-issue.md +53 -0
  91. package/dist/skills/git/references/tracker/linear/fetch-issue.md +14 -0
  92. package/dist/skills/git/references/tracker/linear/fetch-issues-batch.md +15 -0
  93. package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +18 -0
  94. package/dist/skills/git/references/tracker/linear/manage-debt.md +37 -0
  95. package/dist/skills/git/references/tracker/linear/post-wave-report.md +33 -0
  96. package/dist/skills/git/references/tracker/linear/setup-task.md +32 -0
  97. package/dist/skills/git/references/trust-rule.md +7 -0
  98. package/dist/targets/claude-code/installer.js +1213 -31
  99. package/dist/targets/claude-code/legacy.js +5 -0
  100. package/dist/targets/claude-code/post-install.js +303 -104
  101. package/dist/targets/claude-code/tracker-install.js +161 -0
  102. package/package.json +4 -3
  103. package/src/assets/agents/code.md +42 -4
  104. package/src/assets/agents/design.md +1 -1
  105. package/src/assets/agents/git.mds +827 -0
  106. package/src/assets/agents/knowledge.md +1 -1
  107. package/src/assets/agents/learning.md +11 -0
  108. package/src/assets/agents/synthesize.md +1 -1
  109. package/src/assets/agents/test.md +16 -5
  110. package/src/assets/agents/tracker.md +467 -0
  111. package/src/assets/agents/validate.md +7 -5
  112. package/src/assets/commands/_partials/_engine.mds +11 -9
  113. package/src/assets/commands/_partials/_evidence_policy.mds +30 -0
  114. package/src/assets/commands/_partials/_knowledge.mds +2 -2
  115. package/src/assets/commands/_partials/_plan_contract.mds +22 -7
  116. package/src/assets/commands/_partials/_preamble.mds +1 -1
  117. package/src/assets/commands/_partials/_publication.mds +3 -1
  118. package/src/assets/commands/_partials/_ticket_template.mds +3 -2
  119. package/src/assets/commands/_partials/_tracker.mds +18 -0
  120. package/src/assets/commands/_partials/_wave.mds +16 -10
  121. package/src/assets/commands/bug-analysis.mds +15 -5
  122. package/src/assets/commands/code-review.mds +34 -14
  123. package/src/assets/commands/debug.mds +11 -4
  124. package/src/assets/commands/dynamic-build.mds +227 -41
  125. package/src/assets/commands/dynamic-plan.mds +35 -13
  126. package/src/assets/commands/dynamic-tickets.mds +47 -5
  127. package/src/assets/commands/implement.mds +206 -52
  128. package/src/assets/commands/plan.mds +70 -17
  129. package/src/assets/commands/release.md +64 -17
  130. package/src/assets/commands/resolve.mds +126 -56
  131. package/src/assets/mds/git/_pr.mds +331 -0
  132. package/src/assets/mds/git/_references.mds +135 -0
  133. package/src/assets/mds/tracker/_common.mds +156 -0
  134. package/src/assets/mds/tracker/_github.mds +472 -0
  135. package/src/assets/mds/tracker/_jira.mds +407 -0
  136. package/src/assets/mds/tracker/_linear.mds +449 -0
  137. package/src/assets/mds/tracker/_mcp.mds +299 -0
  138. package/src/assets/scripts/hooks/assets/orchestrator-charter.md +5 -8
  139. package/src/assets/scripts/hooks/background-memory-update +14 -9
  140. package/src/assets/scripts/hooks/capture-prompt +6 -2
  141. package/src/assets/scripts/hooks/capture-question +6 -2
  142. package/src/assets/scripts/hooks/capture-turn +6 -2
  143. package/src/assets/scripts/hooks/ensure-devflow-init +1 -1
  144. package/src/assets/scripts/hooks/ensure-proxy +64 -5
  145. package/src/assets/scripts/hooks/ensure-root-gitignore +161 -60
  146. package/src/assets/scripts/hooks/hook-log-init +3 -1
  147. package/src/assets/scripts/hooks/json-helper.cjs +223 -5
  148. package/src/assets/scripts/hooks/lib/project-paths.cjs +1 -1
  149. package/src/assets/scripts/hooks/memory-worker +15 -8
  150. package/src/assets/scripts/hooks/pre-compact-memory +12 -8
  151. package/src/assets/scripts/hooks/preamble +1 -4
  152. package/src/assets/scripts/hooks/queue-append +68 -24
  153. package/src/assets/scripts/hooks/session-start-context +355 -8
  154. package/src/assets/scripts/hooks/session-start-memory +12 -8
  155. package/src/assets/scripts/pr-evidence.cjs +1961 -0
  156. package/src/assets/scripts/redact-secrets.cjs +490 -62
  157. package/src/assets/scripts/release-trace.cjs +1143 -0
  158. package/src/assets/scripts/resolve-evidence-policy.cjs +1065 -0
  159. package/src/assets/scripts/verify-evidence.cjs +1822 -0
  160. package/src/assets/skills/compliance/SKILL.md +2 -0
  161. package/src/assets/skills/docs-framework/SKILL.md +5 -3
  162. package/src/assets/skills/git/SKILL.md +8 -82
  163. package/src/assets/skills/git/references/github-api.md +179 -141
  164. package/src/assets/skills/git/references/patterns.md +11 -12
  165. package/src/assets/skills/review-methodology/SKILL.md +1 -1
  166. package/src/assets/skills/review-methodology/references/patterns.md +6 -61
  167. package/src/assets/skills/review-methodology/references/violations.md +14 -22
  168. package/src/targets/claude-code/templates/settings.json +0 -4
  169. package/src/assets/agents/git.md +0 -938
@@ -88,47 +88,75 @@ export async function writeProxyState(devflowDir, state) {
88
88
  // Pure helpers
89
89
  // ---------------------------------------------------------------------------
90
90
  /**
91
- * Accepted top-level keys for the subswitch 0.2.0 FileConfigSchema (z.strictObject).
92
- * Unknown top-level keys cause a hard relay startup error — never emit them.
91
+ * Top-level keys devflow carries into the routing config. Unknown top-level keys
92
+ * cause a hard relay startup error — never emit them.
93
93
  *
94
- * @D-EFR-4 Subswitch 0.2.0 routing config contract:
95
- * FileConfigSchema is a z.strictObject with exactly 5 accepted top-level keys:
96
- * port, logLevel, anthropic, providers, limits. Unknown keys cause a hard startup
97
- * error — the relay refuses to start. anthropic and limits are themselves
98
- * strictObject + prefault({}), so they may be partially specified.
99
- *
100
- * connectTimeoutMs default: subswitch 0.2.0 applies anthropic.connectTimeoutMs
101
- * as upstream.setTimeout() at anthropic-passthrough.js:86 — a socket *inactivity*
102
- * timeout, not a connect timeout. The 10s default kills any Anthropic request
103
- * whose upstream takes >10s to emit the first response byte — routine for long
104
- * opus requests (confirmed: 99 spurious 504s clustered at 10004-10098ms while
105
- * successful requests ran 25s-99s). Default overridden to 120 000ms; a
106
- * user-specified value always wins.
107
- *
108
- * limits.connectTimeoutMs is a registered LEGACY KEY that causes a hard startup
109
- * error. Strip it from the limits block when preserving existing config.
94
+ * @D-EFR-4 Subswitch 0.5.0 routing config contract:
95
+ * FileConfigSchema is a z.strictObject with exactly 6 accepted top-level keys:
96
+ * port, logLevel, anthropic, providers, limits, codexIngress. Unknown keys cause a
97
+ * hard startup error — the relay refuses to start. anthropic and limits are
98
+ * themselves strictObject + prefault({}), so they may be partially specified.
99
+ * codexIngress is accepted by the relay but deliberately NOT carried: devflow's
100
+ * relay serves Claude Code only, and the ingress block would make it accept native
101
+ * Codex traffic and hold Claude credentials on devflow's behalf.
110
102
  */
111
103
  const ROUTING_CONFIG_ALLOWED_TOP_KEYS = new Set([
112
104
  'port', 'logLevel', 'anthropic', 'providers', 'limits',
113
105
  ]);
114
- /** Default anthropic.connectTimeoutMs injected when not specified by the user (ms). */
115
- export const DEFAULT_ANTHROPIC_CONNECT_TIMEOUT_MS = 120_000;
106
+ /**
107
+ * Sub-keys of a preserved `anthropic` / `limits` block that the pinned relay rejects
108
+ * outright — each is a registered LEGACY KEY, and a legacy key is a hard startup
109
+ * error naming its replacement, not a warning. They are stripped when carrying a
110
+ * user's existing config forward so an upgrade cannot leave the relay unable to boot.
111
+ *
112
+ * Every entry was valid under a version devflow previously pinned, which is what makes
113
+ * it reachable in a real user's file:
114
+ * anthropic.streamIdleTimeoutMs — valid in 0.2.0, removed in 0.3.0 (relay no longer
115
+ * bounds the stream-idle phase on a connected client).
116
+ * limits.connectTimeoutMs — moved to anthropic.connectTimeoutMs in 0.3.0;
117
+ * stripping it loses nothing (relay default governs).
118
+ * limits.maxConcurrentRequests — valid in 0.2.0, removed in 0.3.0 (admission gate
119
+ * removed).
120
+ * limits.maxBodyBytes — valid through 0.3.0, renamed to
121
+ * limits.maxBufferedBodyBytes in 0.4.0. The old spelling is a registered legacy
122
+ * key, so leaving it in place would stop the relay from booting.
123
+ * limits.maxUpstreamSockets — valid in 0.3.0, moved to anthropic.maxUpstreamSockets
124
+ * in 0.4.0's providers.* restructure.
125
+ * limits.streamIdleTimeoutMs — valid in 0.3.0, moved to
126
+ * providers.codex.streamIdleTimeoutMs in 0.4.0.
127
+ * limits.requestTimeoutMs — valid in 0.3.0, moved to
128
+ * providers.codex.requestTimeoutMs in 0.4.0.
129
+ * limits.maxSseEventBytes — valid in 0.3.0, moved to
130
+ * providers.codex.maxSseEventBytes in 0.4.0.
131
+ *
132
+ * Keys retired before the 0.2.0 baseline are deliberately absent: only keys reachable
133
+ * in a config written by a previously pinned devflow version belong on this list.
134
+ */
135
+ const ROUTING_CONFIG_REJECTED_SUBKEYS = {
136
+ anthropic: ['streamIdleTimeoutMs'],
137
+ limits: [
138
+ 'connectTimeoutMs',
139
+ 'maxConcurrentRequests',
140
+ 'maxBodyBytes',
141
+ 'maxUpstreamSockets',
142
+ 'streamIdleTimeoutMs',
143
+ 'requestTimeoutMs',
144
+ 'maxSseEventBytes',
145
+ ],
146
+ };
116
147
  /**
117
148
  * Build the routing config JSON for ~/.devflow/proxy-routing.json.
118
149
  *
119
150
  * Authoritatively sets `port`. Preserves any existing valid `anthropic`,
120
- * `limits`, `logLevel`, and `providers` blocks from `existingContent`,
121
- * filtering out unknown top-level keys and the legacy `limits.connectTimeoutMs`
122
- * field (which causes a hard relay startup error in 0.2.0).
123
- *
124
- * Injects a default `anthropic.connectTimeoutMs` of 120 000ms when the user
125
- * has not set one — see @D-EFR-4 for the rationale.
151
+ * `limits`, `logLevel`, and `providers` blocks from `existingContent`, filtering
152
+ * out unknown top-level keys and every sub-key in ROUTING_CONFIG_REJECTED_SUBKEYS
153
+ * (each one a hard relay startup error). No `anthropic` block is injected when
154
+ * the user has not set one — the relay's own default governs (D-EFR-4).
126
155
  *
127
- * If `existingContent` is missing or malformed, falls back to a clean config
128
- * with only `port` and `anthropic.connectTimeoutMs`.
156
+ * If `existingContent` is missing or malformed, falls back to a port-only config.
129
157
  *
130
158
  * applies ADR-013: pure core-layer function — no I/O.
131
- * @D-EFR-4: see note above for the strict top-key constraint and timeout rationale.
159
+ * @D-EFR-4: see note above for the strict top-key constraint.
132
160
  */
133
161
  export function buildRoutingConfigJson(port, existingContent) {
134
162
  // Parse existing config tolerantly; a malformed file falls back to clean defaults.
@@ -138,7 +166,7 @@ export function buildRoutingConfigJson(port, existingContent) {
138
166
  const parsed = JSON.parse(existingContent);
139
167
  if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
140
168
  for (const [k, v] of Object.entries(parsed)) {
141
- // Only preserve the 5 accepted top-level keys; port is always ours.
169
+ // Only preserve the carried top-level keys; port is always ours.
142
170
  if (ROUTING_CONFIG_ALLOWED_TOP_KEYS.has(k) && k !== 'port') {
143
171
  preserved[k] = v;
144
172
  }
@@ -154,30 +182,36 @@ export function buildRoutingConfigJson(port, existingContent) {
154
182
  if (preserved.logLevel !== undefined) {
155
183
  config.logLevel = preserved.logLevel;
156
184
  }
157
- // Anthropic block: preserve user settings; inject connectTimeoutMs default when absent.
158
- // @D-EFR-4: the 10s upstream inactivity timeout kills long opus requests.
159
- const existingAnthropic = typeof preserved.anthropic === 'object' &&
185
+ // Anthropic block: preserve user settings; strip legacy rejected sub-keys.
186
+ // No default is injected — the relay's own default governs (D-EFR-4).
187
+ if (typeof preserved.anthropic === 'object' &&
160
188
  preserved.anthropic !== null &&
161
- !Array.isArray(preserved.anthropic)
162
- ? { ...preserved.anthropic }
163
- : {};
164
- if (!Object.prototype.hasOwnProperty.call(existingAnthropic, 'connectTimeoutMs')) {
165
- existingAnthropic.connectTimeoutMs = DEFAULT_ANTHROPIC_CONNECT_TIMEOUT_MS;
189
+ !Array.isArray(preserved.anthropic)) {
190
+ const existingAnthropic = { ...preserved.anthropic };
191
+ for (const key of ROUTING_CONFIG_REJECTED_SUBKEYS.anthropic) {
192
+ delete existingAnthropic[key];
193
+ }
194
+ if (Object.keys(existingAnthropic).length > 0) {
195
+ config.anthropic = existingAnthropic;
196
+ }
166
197
  }
167
- config.anthropic = existingAnthropic;
168
198
  // Preserve providers block if present.
169
199
  if (preserved.providers !== undefined) {
170
200
  config.providers = preserved.providers;
171
201
  }
172
- // Preserve limits block if present; strip legacy limits.connectTimeoutMs that
173
- // causes a hard startup error in 0.2.0 (use anthropic.connectTimeoutMs instead).
202
+ // Preserve limits block if present, minus the sub-keys the relay hard-errors on
203
+ // (see ROUTING_CONFIG_REJECTED_SUBKEYS).
174
204
  if (preserved.limits !== undefined) {
175
205
  if (typeof preserved.limits === 'object' &&
176
206
  preserved.limits !== null &&
177
207
  !Array.isArray(preserved.limits)) {
178
208
  const limitsObj = { ...preserved.limits };
179
- delete limitsObj['connectTimeoutMs'];
180
- config.limits = limitsObj;
209
+ for (const key of ROUTING_CONFIG_REJECTED_SUBKEYS.limits) {
210
+ delete limitsObj[key];
211
+ }
212
+ if (Object.keys(limitsObj).length > 0) {
213
+ config.limits = limitsObj;
214
+ }
181
215
  }
182
216
  else {
183
217
  config.limits = preserved.limits;
@@ -210,6 +244,29 @@ export function proxyBaseUrl(port) {
210
244
  // ---------------------------------------------------------------------------
211
245
  // isProxyEnabled — the primary contract other modules consume
212
246
  // ---------------------------------------------------------------------------
247
+ /**
248
+ * Returns true when ~/.devflow/proxy.json exists on disk — i.e. Devflow has
249
+ * previously managed the proxy on this machine.
250
+ *
251
+ * This is the evidence gate used by init and uninstall before stripping
252
+ * ANTHROPIC_BASE_URL / CLAUDE_CODE_DISABLE_UNKNOWN_MODEL_WINDOW_ENFORCEMENT:
253
+ * we only strip those vars when we can prove Devflow wrote them.
254
+ *
255
+ * D-STRIP-1: gate proxy env stripping on devflow-managed evidence.
256
+ * readProxyState() returns Ok(defaultState) on ENOENT — it cannot distinguish
257
+ * "file absent" from "file present with DEFAULT_PROXY_PORT". Callers that need
258
+ * to differentiate must use proxyJsonExists() rather than checking the result
259
+ * of readProxyState().
260
+ */
261
+ export async function proxyJsonExists(devflowDir) {
262
+ try {
263
+ await fs.access(join(devflowDir, 'proxy.json'));
264
+ return true;
265
+ }
266
+ catch {
267
+ return false;
268
+ }
269
+ }
213
270
  /**
214
271
  * Check whether the Devflow proxy is currently enabled.
215
272
  * Returns false when the proxy state file is missing, unreadable, or malformed.
@@ -0,0 +1,136 @@
1
+ import { promises as fs } from 'fs';
2
+ import * as path from 'path';
3
+ /**
4
+ * @file reference-sweep.ts
5
+ *
6
+ * Path-keyed registry-diff sweep for the generated `devflow:git` reference tree.
7
+ *
8
+ * Sibling of {@link sweepOrphanedAssets} in orphan-sweep.ts and deliberately the same
9
+ * {@link SweepResult} shape — `scanned` is the non-vacuity counter, removals and
10
+ * per-item failures are reported rather than thrown (avoids PF-009).
11
+ *
12
+ * What is genuinely new is the KEY. `sweepOrphanedAssets` keys a flat directory by
13
+ * registry name through `mdEntryName`, which cannot express `tracker/{provider}/{op}.md`:
14
+ * two providers may legitimately both carry a `comment.md`, so the registry name has to
15
+ * be the relative PATH, and the walk has to descend.
16
+ *
17
+ * Never writes, only removes (avoids PF-011).
18
+ */
19
+ /**
20
+ * Descent bound for every walk over the generated reference tree — this sweep and the
21
+ * build's own prune, which imports it (`pruneOrphans` in scripts/build-mds.ts). One
22
+ * tree, one bound: two walkers each carrying their own literal is how they come to
23
+ * disagree on both the number of levels and what happens at the last one.
24
+ *
25
+ * `depth` counts the walked root as 0 and the bound is the deepest directory a walk may
26
+ * descend INTO, so `depth > MAX_REFERENCE_SWEEP_DEPTH` is the breach — the comparison
27
+ * the build's walks already use. The installed tree is two levels deep
28
+ * (`tracker/{provider}/{op}.md`), so 8 is generous. The bound exists because an
29
+ * unbounded recursion over a directory neither walker owns would spin on a symlink loop
30
+ * rather than fail — every loop has an explicit upper bound.
31
+ *
32
+ * The two walkers answer a breach differently by design, and both answer out loud: the
33
+ * build throws (a generated tree that deep is a build bug, and dist/ is still the
34
+ * build's own to fail), while this sweep records the unvisited subtree in `failed`
35
+ * (avoids PF-009 — an install is not abandoned over one subtree). Neither returns
36
+ * quietly: a subtree the walk never entered must not be summarised as converged.
37
+ */
38
+ export const MAX_REFERENCE_SWEEP_DEPTH = 8;
39
+ /**
40
+ * Remove everything under `root` that the manifest does not name.
41
+ *
42
+ * @param root - Directory to converge (the installed `references/tracker/` tree).
43
+ * @param knownRelPaths - POSIX paths relative to `root` that must survive. Derived from
44
+ * the build's own module registries by the caller — never hand-listed.
45
+ *
46
+ * @returns A {@link SweepResult} whose `removed` entries are POSIX relative paths. A
47
+ * directory into which no manifest path descends is removed WHOLE and reported by its
48
+ * own relative path — leaving it empty would be a convergence that stops one step
49
+ * short, and an empty provider directory is indistinguishable from a provider whose
50
+ * references failed to install. A subtree left unswept because it breached
51
+ * {@link MAX_REFERENCE_SWEEP_DEPTH} is reported in `failed` under its own relative
52
+ * path, for the same reason: everything this sweep did not converge is named.
53
+ *
54
+ * A missing or unreadable `root` is a no-op, not an error: the overlay creates the tree
55
+ * it converges, so an absent one simply means there is nothing to prune yet.
56
+ */
57
+ export async function sweepOrphanedReferences(root, knownRelPaths) {
58
+ const acc = { scanned: 0, removed: [], failed: [] };
59
+ await sweepDirectory(root, '', 0, knownRelPaths, directoryPrefixes(knownRelPaths), acc);
60
+ return { scanned: acc.scanned, removed: acc.removed, failed: acc.failed };
61
+ }
62
+ async function sweepDirectory(dir, prefix, depth, known, knownDirPrefixes, acc) {
63
+ if (depth > MAX_REFERENCE_SWEEP_DEPTH) {
64
+ // A breached bound means this subtree is never visited, so any orphan inside it
65
+ // survives. Report it through the same channel as a failed removal: a silent return
66
+ // would let the install summary claim convergence over ground never covered.
67
+ acc.failed.push({
68
+ name: prefix || dir,
69
+ error: new Error(`${prefix || dir}: sweep descent exceeds the bound of ${MAX_REFERENCE_SWEEP_DEPTH} ` +
70
+ `levels — this subtree was not swept and any orphans under it survive.`),
71
+ });
72
+ return;
73
+ }
74
+ let entries;
75
+ try {
76
+ entries = await fs.readdir(dir, { withFileTypes: true });
77
+ }
78
+ catch {
79
+ return; /* absent or unreadable — not an error (avoids PF-009) */
80
+ }
81
+ for (const entry of entries) {
82
+ const relPath = prefix === '' ? entry.name : `${prefix}/${entry.name}`;
83
+ const fullPath = path.join(dir, entry.name);
84
+ // A real subdirectory is either an ancestor of something the manifest names — in
85
+ // which case descend — or dead weight, in which case take the whole subtree.
86
+ // isDirectory() is false for a symlink-to-dir, so a planted link is treated as a
87
+ // leaf and removed rather than followed.
88
+ if (entry.isDirectory()) {
89
+ if (knownDirPrefixes.has(`${relPath}/`)) {
90
+ await sweepDirectory(fullPath, relPath, depth + 1, known, knownDirPrefixes, acc);
91
+ continue;
92
+ }
93
+ acc.scanned++;
94
+ try {
95
+ await fs.rm(fullPath, { recursive: true, force: true });
96
+ acc.removed.push(relPath);
97
+ }
98
+ catch (err) {
99
+ acc.failed.push({ name: relPath, error: err }); /* per-item isolation (avoids PF-009) */
100
+ }
101
+ continue;
102
+ }
103
+ acc.scanned++;
104
+ if (known.has(relPath))
105
+ continue;
106
+ try {
107
+ await fs.rm(fullPath, { force: true });
108
+ acc.removed.push(relPath);
109
+ }
110
+ catch (err) {
111
+ acc.failed.push({ name: relPath, error: err }); /* per-item isolation (avoids PF-009) */
112
+ }
113
+ }
114
+ }
115
+ /**
116
+ * Every directory prefix some manifest path sits under, trailing slash included:
117
+ * `tracker/github/comment.md` contributes `tracker/` and `tracker/github/`.
118
+ *
119
+ * Built once per sweep so "does anything the manifest names live under this directory?"
120
+ * costs one `has` per entry instead of a scan of the whole manifest per entry. The
121
+ * answer is identical to that scan: `dir/` is a member exactly when some manifest path
122
+ * starts with `dir/`.
123
+ *
124
+ * Both loops are bounded by their own input — the inner one walks separators from a
125
+ * strictly increasing offset, so it terminates at the last one in the path.
126
+ */
127
+ function directoryPrefixes(known) {
128
+ const prefixes = new Set();
129
+ for (const p of known) {
130
+ for (let cut = p.indexOf('/'); cut !== -1; cut = p.indexOf('/', cut + 1)) {
131
+ prefixes.add(p.slice(0, cut + 1));
132
+ }
133
+ }
134
+ return prefixes;
135
+ }
136
+ //# sourceMappingURL=reference-sweep.js.map