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.
- package/CHANGELOG.md +177 -0
- package/README.md +86 -18
- package/dist/agents/git.md +824 -0
- package/dist/cli/commands/agents.js +6 -1
- package/dist/cli/commands/attribution-prompts.js +144 -0
- package/dist/cli/commands/compliance-prompts.js +7 -11
- package/dist/cli/commands/compliance.js +23 -1
- package/dist/cli/commands/init-seed.js +69 -30
- package/dist/cli/commands/init.js +551 -77
- package/dist/cli/commands/install-report.js +205 -0
- package/dist/cli/commands/knowledge/index.js +2 -2
- package/dist/cli/commands/knowledge/toggle.js +27 -37
- package/dist/cli/commands/learning.js +37 -30
- package/dist/cli/commands/memory.js +79 -69
- package/dist/cli/commands/prompt-io.js +38 -0
- package/dist/cli/commands/proxy.js +91 -31
- package/dist/cli/commands/security.js +76 -16
- package/dist/cli/commands/skills.js +53 -7
- package/dist/cli/commands/tracker-prompts.js +145 -0
- package/dist/cli/commands/tracker.js +405 -0
- package/dist/cli/commands/uninstall.js +233 -77
- package/dist/cli.js +2 -0
- package/dist/commands/bug-analysis.md +22 -4
- package/dist/commands/code-review.md +44 -15
- package/dist/commands/debug.md +20 -6
- package/dist/commands/dynamic-build.md +289 -67
- package/dist/commands/dynamic-plan.md +60 -21
- package/dist/commands/dynamic-profile.md +1 -1
- package/dist/commands/dynamic-tickets.md +58 -8
- package/dist/commands/explore.md +2 -2
- package/dist/commands/implement.md +241 -53
- package/dist/commands/plan.md +88 -17
- package/dist/commands/release.md +64 -17
- package/dist/commands/resolve.md +138 -58
- package/dist/commands/self-review.md +2 -2
- package/dist/core/agent-models.js +55 -12
- package/dist/core/assets.js +58 -2
- package/dist/core/evidence-policy.js +147 -0
- package/dist/core/feature-config.js +130 -64
- package/dist/core/feature-switch.js +112 -0
- package/dist/core/flags.js +149 -8
- package/dist/core/manifest.js +33 -7
- package/dist/core/mds-variants.js +861 -0
- package/dist/core/model-discovery.js +12 -1
- package/dist/core/plugins.js +357 -9
- package/dist/core/project-paths.js +1 -1
- package/dist/core/proxy-log.js +8 -4
- package/dist/core/proxy-state.js +100 -43
- package/dist/core/reference-sweep.js +136 -0
- package/dist/core/tracker.js +407 -0
- package/dist/skills/git/references/decision-markers.md +19 -0
- package/dist/skills/git/references/learn-conventions.md +56 -0
- package/dist/skills/git/references/pr/check-ci-status.md +14 -0
- package/dist/skills/git/references/pr/check-merge-readiness.md +28 -0
- package/dist/skills/git/references/pr/ensure-pr-ready.md +24 -0
- package/dist/skills/git/references/pr/fetch-review-threads.md +22 -0
- package/dist/skills/git/references/pr/post-resolution-summary.md +40 -0
- package/dist/skills/git/references/pr/post-review-summary.md +42 -0
- package/dist/skills/git/references/pr/resolve-review-threads.md +35 -0
- package/dist/skills/git/references/pr/update-pr-evidence.md +14 -0
- package/dist/skills/git/references/pr/validate-branch.md +18 -0
- package/dist/skills/git/references/publication-gate.md +13 -0
- package/dist/skills/git/references/tracker/_mcp.md +153 -0
- package/dist/skills/git/references/tracker/github/associate-release.md +18 -0
- package/dist/skills/git/references/tracker/github/backlink-shipped-issues.md +40 -0
- package/dist/skills/git/references/tracker/github/create-release.md +11 -0
- package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +16 -0
- package/dist/skills/git/references/tracker/github/ensure-traceable-issue.md +69 -0
- package/dist/skills/git/references/tracker/github/fetch-issue.md +32 -0
- package/dist/skills/git/references/tracker/github/fetch-issues-batch.md +17 -0
- package/dist/skills/git/references/tracker/github/gather-release-evidence.md +19 -0
- package/dist/skills/git/references/tracker/github/manage-debt.md +101 -0
- package/dist/skills/git/references/tracker/github/post-wave-report.md +28 -0
- package/dist/skills/git/references/tracker/github/setup-task.md +26 -0
- package/dist/skills/git/references/tracker/jira/associate-release.md +18 -0
- package/dist/skills/git/references/tracker/jira/backlink-shipped-issues.md +49 -0
- package/dist/skills/git/references/tracker/jira/create-release.md +17 -0
- package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +22 -0
- package/dist/skills/git/references/tracker/jira/ensure-traceable-issue.md +53 -0
- package/dist/skills/git/references/tracker/jira/fetch-issue.md +14 -0
- package/dist/skills/git/references/tracker/jira/fetch-issues-batch.md +15 -0
- package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +18 -0
- package/dist/skills/git/references/tracker/jira/manage-debt.md +37 -0
- package/dist/skills/git/references/tracker/jira/post-wave-report.md +33 -0
- package/dist/skills/git/references/tracker/jira/setup-task.md +31 -0
- package/dist/skills/git/references/tracker/linear/associate-release.md +18 -0
- package/dist/skills/git/references/tracker/linear/backlink-shipped-issues.md +53 -0
- package/dist/skills/git/references/tracker/linear/create-release.md +17 -0
- package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +22 -0
- package/dist/skills/git/references/tracker/linear/ensure-traceable-issue.md +53 -0
- package/dist/skills/git/references/tracker/linear/fetch-issue.md +14 -0
- package/dist/skills/git/references/tracker/linear/fetch-issues-batch.md +15 -0
- package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +18 -0
- package/dist/skills/git/references/tracker/linear/manage-debt.md +37 -0
- package/dist/skills/git/references/tracker/linear/post-wave-report.md +33 -0
- package/dist/skills/git/references/tracker/linear/setup-task.md +32 -0
- package/dist/skills/git/references/trust-rule.md +7 -0
- package/dist/targets/claude-code/installer.js +1213 -31
- package/dist/targets/claude-code/legacy.js +5 -0
- package/dist/targets/claude-code/post-install.js +303 -104
- package/dist/targets/claude-code/tracker-install.js +161 -0
- package/package.json +4 -3
- package/src/assets/agents/code.md +42 -4
- package/src/assets/agents/design.md +1 -1
- package/src/assets/agents/git.mds +827 -0
- package/src/assets/agents/knowledge.md +1 -1
- package/src/assets/agents/learning.md +11 -0
- package/src/assets/agents/synthesize.md +1 -1
- package/src/assets/agents/test.md +16 -5
- package/src/assets/agents/tracker.md +467 -0
- package/src/assets/agents/validate.md +7 -5
- package/src/assets/commands/_partials/_engine.mds +11 -9
- package/src/assets/commands/_partials/_evidence_policy.mds +30 -0
- package/src/assets/commands/_partials/_knowledge.mds +2 -2
- package/src/assets/commands/_partials/_plan_contract.mds +22 -7
- package/src/assets/commands/_partials/_preamble.mds +1 -1
- package/src/assets/commands/_partials/_publication.mds +3 -1
- package/src/assets/commands/_partials/_ticket_template.mds +3 -2
- package/src/assets/commands/_partials/_tracker.mds +18 -0
- package/src/assets/commands/_partials/_wave.mds +16 -10
- package/src/assets/commands/bug-analysis.mds +15 -5
- package/src/assets/commands/code-review.mds +34 -14
- package/src/assets/commands/debug.mds +11 -4
- package/src/assets/commands/dynamic-build.mds +227 -41
- package/src/assets/commands/dynamic-plan.mds +35 -13
- package/src/assets/commands/dynamic-tickets.mds +47 -5
- package/src/assets/commands/implement.mds +206 -52
- package/src/assets/commands/plan.mds +70 -17
- package/src/assets/commands/release.md +64 -17
- package/src/assets/commands/resolve.mds +126 -56
- package/src/assets/mds/git/_pr.mds +331 -0
- package/src/assets/mds/git/_references.mds +135 -0
- package/src/assets/mds/tracker/_common.mds +156 -0
- package/src/assets/mds/tracker/_github.mds +472 -0
- package/src/assets/mds/tracker/_jira.mds +407 -0
- package/src/assets/mds/tracker/_linear.mds +449 -0
- package/src/assets/mds/tracker/_mcp.mds +299 -0
- package/src/assets/scripts/hooks/assets/orchestrator-charter.md +5 -8
- package/src/assets/scripts/hooks/background-memory-update +14 -9
- package/src/assets/scripts/hooks/capture-prompt +6 -2
- package/src/assets/scripts/hooks/capture-question +6 -2
- package/src/assets/scripts/hooks/capture-turn +6 -2
- package/src/assets/scripts/hooks/ensure-devflow-init +1 -1
- package/src/assets/scripts/hooks/ensure-proxy +64 -5
- package/src/assets/scripts/hooks/ensure-root-gitignore +161 -60
- package/src/assets/scripts/hooks/hook-log-init +3 -1
- package/src/assets/scripts/hooks/json-helper.cjs +223 -5
- package/src/assets/scripts/hooks/lib/project-paths.cjs +1 -1
- package/src/assets/scripts/hooks/memory-worker +15 -8
- package/src/assets/scripts/hooks/pre-compact-memory +12 -8
- package/src/assets/scripts/hooks/preamble +1 -4
- package/src/assets/scripts/hooks/queue-append +68 -24
- package/src/assets/scripts/hooks/session-start-context +355 -8
- package/src/assets/scripts/hooks/session-start-memory +12 -8
- package/src/assets/scripts/pr-evidence.cjs +1961 -0
- package/src/assets/scripts/redact-secrets.cjs +490 -62
- package/src/assets/scripts/release-trace.cjs +1143 -0
- package/src/assets/scripts/resolve-evidence-policy.cjs +1065 -0
- package/src/assets/scripts/verify-evidence.cjs +1822 -0
- package/src/assets/skills/compliance/SKILL.md +2 -0
- package/src/assets/skills/docs-framework/SKILL.md +5 -3
- package/src/assets/skills/git/SKILL.md +8 -82
- package/src/assets/skills/git/references/github-api.md +179 -141
- package/src/assets/skills/git/references/patterns.md +11 -12
- package/src/assets/skills/review-methodology/SKILL.md +1 -1
- package/src/assets/skills/review-methodology/references/patterns.md +6 -61
- package/src/assets/skills/review-methodology/references/violations.md +14 -22
- package/src/targets/claude-code/templates/settings.json +0 -4
- package/src/assets/agents/git.md +0 -938
package/dist/core/proxy-state.js
CHANGED
|
@@ -88,47 +88,75 @@ export async function writeProxyState(devflowDir, state) {
|
|
|
88
88
|
// Pure helpers
|
|
89
89
|
// ---------------------------------------------------------------------------
|
|
90
90
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
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.
|
|
95
|
-
* FileConfigSchema is a z.strictObject with exactly
|
|
96
|
-
* port, logLevel, anthropic, providers, limits. Unknown keys cause a
|
|
97
|
-
* error — the relay refuses to start. anthropic and limits are
|
|
98
|
-
* strictObject + prefault({}), so they may be partially specified.
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
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
|
-
/**
|
|
115
|
-
|
|
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
|
-
*
|
|
122
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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;
|
|
158
|
-
//
|
|
159
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
|
173
|
-
//
|
|
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
|
-
|
|
180
|
-
|
|
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
|