@devrouter/cli 0.0.48 → 0.0.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/devrouter.js +82 -24
- package/package.json +1 -1
- package/upgrade-prompts/0.0.49.md +26 -0
- package/upgrade-prompts/0.0.50.md +26 -0
package/dist/devrouter.js
CHANGED
|
@@ -2668,7 +2668,7 @@ function loadRepoConfig(repoPath) {
|
|
|
2668
2668
|
const config = parseConfig(parsed ?? {}, configPath);
|
|
2669
2669
|
const requiredVersion = config.devrouter?.version;
|
|
2670
2670
|
if (requiredVersion && !hasWarnedVersionMismatch) {
|
|
2671
|
-
const cliVersion = true ? "0.0.
|
|
2671
|
+
const cliVersion = true ? "0.0.50" : "0.0.0-dev";
|
|
2672
2672
|
if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
|
|
2673
2673
|
hasWarnedVersionMismatch = true;
|
|
2674
2674
|
process.stderr.write(
|
|
@@ -8986,7 +8986,7 @@ async function buildDoctorReport(options = {}) {
|
|
|
8986
8986
|
const config = runtimeConfig.config;
|
|
8987
8987
|
loadedConfig = config;
|
|
8988
8988
|
loadedWorkspace = runtimeConfig.workspace;
|
|
8989
|
-
const cliVersion = true ? "0.0.
|
|
8989
|
+
const cliVersion = true ? "0.0.50" : "0.0.0-dev";
|
|
8990
8990
|
const configVersion = config.devrouter?.version;
|
|
8991
8991
|
if (configVersion && cliVersion !== "0.0.0-dev" && compareSemver(configVersion, cliVersion) > 0) {
|
|
8992
8992
|
addCheck(checks, {
|
|
@@ -9741,7 +9741,7 @@ function inspectRouterApi(protocol) {
|
|
|
9741
9741
|
"--fail",
|
|
9742
9742
|
"--max-time",
|
|
9743
9743
|
"2",
|
|
9744
|
-
`${ROUTER_API_BASE}/${protocol}/routers`
|
|
9744
|
+
`${ROUTER_API_BASE}/${protocol}/routers?per_page=${ROUTER_API_PAGE_SIZE}`
|
|
9745
9745
|
],
|
|
9746
9746
|
{ encoding: "utf-8" }
|
|
9747
9747
|
);
|
|
@@ -9762,6 +9762,7 @@ function inspectRouterApi(protocol) {
|
|
|
9762
9762
|
}
|
|
9763
9763
|
return {
|
|
9764
9764
|
ok: true,
|
|
9765
|
+
reachedPageLimit: value.length >= ROUTER_API_PAGE_SIZE,
|
|
9765
9766
|
names: new Set(
|
|
9766
9767
|
value.flatMap((item) => {
|
|
9767
9768
|
if (!item || typeof item !== "object") return [];
|
|
@@ -9771,8 +9772,8 @@ function inspectRouterApi(protocol) {
|
|
|
9771
9772
|
)
|
|
9772
9773
|
};
|
|
9773
9774
|
}
|
|
9774
|
-
function inspectExpectedRoutes(routes) {
|
|
9775
|
-
const
|
|
9775
|
+
function inspectExpectedRoutes(routes, expectation) {
|
|
9776
|
+
const mismatched = [];
|
|
9776
9777
|
const failures = [];
|
|
9777
9778
|
for (const protocol of ["http", "tcp"]) {
|
|
9778
9779
|
const expected = routes.filter((route) => (route.protocol ?? "http") === protocol).map((route) => buildHostRouteRouterName(route.repoPath, route.name));
|
|
@@ -9780,22 +9781,34 @@ function inspectExpectedRoutes(routes) {
|
|
|
9780
9781
|
const result = inspectRouterApi(protocol);
|
|
9781
9782
|
if (!result.ok) {
|
|
9782
9783
|
failures.push(result.details);
|
|
9783
|
-
|
|
9784
|
+
mismatched.push(...expected);
|
|
9784
9785
|
continue;
|
|
9785
9786
|
}
|
|
9786
|
-
|
|
9787
|
+
const unexpected = expected.filter(
|
|
9788
|
+
(name) => expectation === "loaded" ? !result.names.has(name) : result.names.has(name)
|
|
9789
|
+
);
|
|
9790
|
+
const boundedAbsenceIsUncertain = result.reachedPageLimit && (expectation === "loaded" ? unexpected.length > 0 : unexpected.length === 0);
|
|
9791
|
+
if (boundedAbsenceIsUncertain) {
|
|
9792
|
+
failures.push(
|
|
9793
|
+
`${protocol.toUpperCase()} router API reached the ${ROUTER_API_PAGE_SIZE}-entry safety limit`
|
|
9794
|
+
);
|
|
9795
|
+
}
|
|
9796
|
+
mismatched.push(...unexpected);
|
|
9797
|
+
if (expectation === "removed" && unexpected.length === 0 && result.reachedPageLimit) {
|
|
9798
|
+
mismatched.push(...expected);
|
|
9799
|
+
}
|
|
9787
9800
|
}
|
|
9788
|
-
return
|
|
9801
|
+
return mismatched.length === 0 ? { ok: true } : {
|
|
9789
9802
|
ok: false,
|
|
9790
|
-
|
|
9791
|
-
details: failures.length > 0 ? failures.join("; ") : "router names are absent"
|
|
9803
|
+
routes: mismatched,
|
|
9804
|
+
details: failures.length > 0 ? failures.join("; ") : expectation === "loaded" ? "router names are absent" : "router names are still present"
|
|
9792
9805
|
};
|
|
9793
9806
|
}
|
|
9794
|
-
async function waitForExpectedRoutes(routes, timeoutMs, pollIntervalMs) {
|
|
9807
|
+
async function waitForExpectedRoutes(routes, expectation, timeoutMs, pollIntervalMs) {
|
|
9795
9808
|
const deadline = Date.now() + timeoutMs;
|
|
9796
9809
|
let result;
|
|
9797
9810
|
do {
|
|
9798
|
-
result = inspectExpectedRoutes(routes);
|
|
9811
|
+
result = inspectExpectedRoutes(routes, expectation);
|
|
9799
9812
|
if (result.ok) return result;
|
|
9800
9813
|
if (Date.now() < deadline) {
|
|
9801
9814
|
await sleep(Math.min(pollIntervalMs, Math.max(1, deadline - Date.now())));
|
|
@@ -9803,12 +9816,17 @@ async function waitForExpectedRoutes(routes, timeoutMs, pollIntervalMs) {
|
|
|
9803
9816
|
} while (Date.now() < deadline);
|
|
9804
9817
|
return result;
|
|
9805
9818
|
}
|
|
9806
|
-
async function
|
|
9819
|
+
async function ensureTraefikRouteExpectation(routes, expectation, options) {
|
|
9807
9820
|
if (routes.length === 0) return { restarted: false };
|
|
9808
9821
|
const initialTimeoutMs = options.initialTimeoutMs ?? DEFAULT_INITIAL_TIMEOUT_MS;
|
|
9809
9822
|
const recoveryTimeoutMs = options.recoveryTimeoutMs ?? DEFAULT_RECOVERY_TIMEOUT_MS;
|
|
9810
9823
|
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
9811
|
-
const initial = await waitForExpectedRoutes(
|
|
9824
|
+
const initial = await waitForExpectedRoutes(
|
|
9825
|
+
routes,
|
|
9826
|
+
expectation,
|
|
9827
|
+
initialTimeoutMs,
|
|
9828
|
+
pollIntervalMs
|
|
9829
|
+
);
|
|
9812
9830
|
if (initial.ok) return { restarted: false };
|
|
9813
9831
|
import_node_fs24.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
|
|
9814
9832
|
return withFileLock(
|
|
@@ -9820,18 +9838,30 @@ async function ensureTraefikRoutesLoaded(routes, options = {}) {
|
|
|
9820
9838
|
onWait: createStderrWaitReporter("Traefik route reload recovery", "shared router")
|
|
9821
9839
|
},
|
|
9822
9840
|
async () => {
|
|
9823
|
-
const recheck = await waitForExpectedRoutes(routes, 0, pollIntervalMs);
|
|
9841
|
+
const recheck = await waitForExpectedRoutes(routes, expectation, 0, pollIntervalMs);
|
|
9824
9842
|
if (recheck.ok) return { restarted: false };
|
|
9825
9843
|
restartRouterStack();
|
|
9826
|
-
const recovered = await waitForExpectedRoutes(
|
|
9844
|
+
const recovered = await waitForExpectedRoutes(
|
|
9845
|
+
routes,
|
|
9846
|
+
expectation,
|
|
9847
|
+
recoveryTimeoutMs,
|
|
9848
|
+
pollIntervalMs
|
|
9849
|
+
);
|
|
9827
9850
|
if (recovered.ok) return { restarted: true };
|
|
9851
|
+
const action2 = expectation === "loaded" ? "load" : "remove";
|
|
9828
9852
|
throw new Error(
|
|
9829
|
-
`Traefik did not
|
|
9853
|
+
`Traefik did not ${action2} file-provider routes after one restart: ${recovered.routes.join(", ")} (${recovered.details}). Inspect: devrouter logs --tail 100`
|
|
9830
9854
|
);
|
|
9831
9855
|
}
|
|
9832
9856
|
);
|
|
9833
9857
|
}
|
|
9834
|
-
|
|
9858
|
+
async function ensureTraefikRoutesLoaded(routes, options = {}) {
|
|
9859
|
+
return ensureTraefikRouteExpectation(routes, "loaded", options);
|
|
9860
|
+
}
|
|
9861
|
+
async function ensureTraefikRoutesRemoved(routes, options = {}) {
|
|
9862
|
+
return ensureTraefikRouteExpectation(routes, "removed", options);
|
|
9863
|
+
}
|
|
9864
|
+
var import_node_child_process20, import_node_fs24, import_node_path25, ROUTER_API_BASE, ROUTER_RELOAD_LOCK_FILE, DEFAULT_INITIAL_TIMEOUT_MS, DEFAULT_RECOVERY_TIMEOUT_MS, DEFAULT_POLL_INTERVAL_MS, ROUTER_API_PAGE_SIZE;
|
|
9835
9865
|
var init_traefik_route_health = __esm({
|
|
9836
9866
|
"src/core/traefik-route-health.ts"() {
|
|
9837
9867
|
"use strict";
|
|
@@ -9846,6 +9876,7 @@ var init_traefik_route_health = __esm({
|
|
|
9846
9876
|
DEFAULT_INITIAL_TIMEOUT_MS = 3e3;
|
|
9847
9877
|
DEFAULT_RECOVERY_TIMEOUT_MS = 1e4;
|
|
9848
9878
|
DEFAULT_POLL_INTERVAL_MS = 250;
|
|
9879
|
+
ROUTER_API_PAGE_SIZE = 1e3;
|
|
9849
9880
|
}
|
|
9850
9881
|
});
|
|
9851
9882
|
|
|
@@ -9987,6 +10018,13 @@ function routeInputFromState(route) {
|
|
|
9987
10018
|
workspace: route.workspace
|
|
9988
10019
|
};
|
|
9989
10020
|
}
|
|
10021
|
+
function routeReferenceKey(route) {
|
|
10022
|
+
return `${route.repoPath}\0${route.name}\0${route.protocol ?? "http"}`;
|
|
10023
|
+
}
|
|
10024
|
+
function removedRoutesForReplacement(previousRoutes, nextRoutes) {
|
|
10025
|
+
const nextKeys = new Set(nextRoutes.map(routeReferenceKey));
|
|
10026
|
+
return previousRoutes.filter((route) => !nextKeys.has(routeReferenceKey(route)));
|
|
10027
|
+
}
|
|
9990
10028
|
function isWildcard2(values) {
|
|
9991
10029
|
return values?.length === 1 && values[0] === "*";
|
|
9992
10030
|
}
|
|
@@ -10298,6 +10336,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10298
10336
|
let managedComposeProject;
|
|
10299
10337
|
let managedContainer;
|
|
10300
10338
|
let previousRoutes = [];
|
|
10339
|
+
let candidateRoutes = [];
|
|
10301
10340
|
let candidateRoutesPublished = false;
|
|
10302
10341
|
let transitionPhase = "validation";
|
|
10303
10342
|
let managedProcessRegistry = [];
|
|
@@ -10327,6 +10366,9 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10327
10366
|
options.profile
|
|
10328
10367
|
);
|
|
10329
10368
|
const managedRuntime = runtime.config.managedRuntime;
|
|
10369
|
+
previousRoutes = listHostRouteState().filter(
|
|
10370
|
+
(route) => sameWorkspacePath(route.repoPath, repoPath)
|
|
10371
|
+
);
|
|
10330
10372
|
managedRuntimeConfig = managedRuntime ? runtime.config : void 0;
|
|
10331
10373
|
managedProcessRegistry = managedRuntime?.processes ?? [];
|
|
10332
10374
|
const desiredProcesses = managedRuntime ? desiredManagedProcesses(managedRuntime, runtime.resolvedProfile) : [];
|
|
@@ -10355,9 +10397,6 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10355
10397
|
)) {
|
|
10356
10398
|
throw new Error("Managed runtime state contains an unregistered process marker.");
|
|
10357
10399
|
}
|
|
10358
|
-
previousRoutes = listHostRouteState().filter(
|
|
10359
|
-
(route) => sameWorkspacePath(route.repoPath, repoPath)
|
|
10360
|
-
);
|
|
10361
10400
|
if (managedPostStart.kind !== "runtime") {
|
|
10362
10401
|
throw new Error(
|
|
10363
10402
|
"managedRuntime requires the runtime managed post-start adapter for exact process reconciliation."
|
|
@@ -10548,8 +10587,13 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10548
10587
|
runtime.config,
|
|
10549
10588
|
target.workspace
|
|
10550
10589
|
);
|
|
10590
|
+
candidateRoutes = publication.routes;
|
|
10551
10591
|
candidateRoutesPublished = true;
|
|
10552
10592
|
await ensureTraefikRoutesLoaded(publication.routes, routeLoadOptions);
|
|
10593
|
+
await ensureTraefikRoutesRemoved(
|
|
10594
|
+
removedRoutesForReplacement(previousRoutes.map(routeInputFromState), publication.routes),
|
|
10595
|
+
routeLoadOptions
|
|
10596
|
+
);
|
|
10553
10597
|
try {
|
|
10554
10598
|
await waitForHttpRoutes(
|
|
10555
10599
|
repoPath,
|
|
@@ -10561,6 +10605,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10561
10605
|
throw error;
|
|
10562
10606
|
}
|
|
10563
10607
|
replaceHostRoutesForRepo(repoPath, []);
|
|
10608
|
+
await ensureTraefikRoutesRemoved(candidateRoutes, routeLoadOptions);
|
|
10564
10609
|
if (!target.hadExactDevpod || recreated) {
|
|
10565
10610
|
throw error;
|
|
10566
10611
|
}
|
|
@@ -10750,6 +10795,10 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10750
10795
|
const rollbackRoutes = previousRoutes.map(routeInputFromState);
|
|
10751
10796
|
replaceHostRoutesForRepo(repoPath, rollbackRoutes);
|
|
10752
10797
|
await ensureTraefikRoutesLoaded(rollbackRoutes, routeLoadOptions);
|
|
10798
|
+
await ensureTraefikRoutesRemoved(
|
|
10799
|
+
removedRoutesForReplacement(candidateRoutes, rollbackRoutes),
|
|
10800
|
+
routeLoadOptions
|
|
10801
|
+
);
|
|
10753
10802
|
} catch (rollbackError) {
|
|
10754
10803
|
rollbackErrors.push(
|
|
10755
10804
|
`routes: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`
|
|
@@ -10795,6 +10844,10 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10795
10844
|
const rollbackRoutes = previousRoutes.map(routeInputFromState);
|
|
10796
10845
|
replaceHostRoutesForRepo(repoPath, rollbackRoutes);
|
|
10797
10846
|
await ensureTraefikRoutesLoaded(rollbackRoutes, routeLoadOptions);
|
|
10847
|
+
await ensureTraefikRoutesRemoved(
|
|
10848
|
+
removedRoutesForReplacement(candidateRoutes, rollbackRoutes),
|
|
10849
|
+
routeLoadOptions
|
|
10850
|
+
);
|
|
10798
10851
|
} catch (rollbackError) {
|
|
10799
10852
|
rollbackErrors.push(
|
|
10800
10853
|
`routes: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`
|
|
@@ -10813,6 +10866,7 @@ async function workspaceEnsure(requestedRepoPath, options = {}) {
|
|
|
10813
10866
|
if (environmentStarted) {
|
|
10814
10867
|
try {
|
|
10815
10868
|
replaceHostRoutesForRepo(repoPath, []);
|
|
10869
|
+
await ensureTraefikRoutesRemoved(candidateRoutes, routeLoadOptions);
|
|
10816
10870
|
} catch (cleanupError) {
|
|
10817
10871
|
const original = error instanceof Error ? error.message : String(error);
|
|
10818
10872
|
const cleanup = cleanupError instanceof Error ? cleanupError.message : String(cleanupError);
|
|
@@ -11181,12 +11235,13 @@ function workspaceLs(repoPath) {
|
|
|
11181
11235
|
};
|
|
11182
11236
|
});
|
|
11183
11237
|
}
|
|
11184
|
-
function mutateWorkspaceRuntime(action2, resolved, worktrees, quiet = false) {
|
|
11238
|
+
async function mutateWorkspaceRuntime(action2, resolved, worktrees, quiet = false) {
|
|
11185
11239
|
const devpods = listDevpodWorkspaces(resolved.worktreePath);
|
|
11186
11240
|
assertDevpodTargetSafe(resolved, worktrees, devpods);
|
|
11187
11241
|
const devpodId = resolved.record?.devpodId ?? resolved.workspace;
|
|
11188
11242
|
const mutation = action2 === "stop" ? stopOwnedDevpodWorkspace(devpodId, resolved.worktreePath) : deleteOwnedDevpodWorkspace(devpodId, resolved.worktreePath);
|
|
11189
11243
|
const routes = removeWorkspaceRoutesForWorktree(resolved.workspace, resolved.worktreePath);
|
|
11244
|
+
await ensureTraefikRoutesRemoved(routes);
|
|
11190
11245
|
if (!quiet) {
|
|
11191
11246
|
process.stdout.write(
|
|
11192
11247
|
`Freed ${routes.length} route(s) for workspace '${resolved.workspace}'.
|
|
@@ -11210,7 +11265,7 @@ async function runWorkspaceLifecycle(action2, target, opts = {}) {
|
|
|
11210
11265
|
if (removeWorktree) {
|
|
11211
11266
|
assertFullDownPreflight(mainRepo, resolved);
|
|
11212
11267
|
}
|
|
11213
|
-
const result = mutateWorkspaceRuntime(
|
|
11268
|
+
const result = await mutateWorkspaceRuntime(
|
|
11214
11269
|
action2 === "stop" ? "stop" : "delete",
|
|
11215
11270
|
resolved,
|
|
11216
11271
|
worktrees,
|
|
@@ -11282,6 +11337,7 @@ var init_workspace_lifecycle = __esm({
|
|
|
11282
11337
|
init_devpod_workspaces();
|
|
11283
11338
|
init_repo_config();
|
|
11284
11339
|
init_route_state();
|
|
11340
|
+
init_traefik_route_health();
|
|
11285
11341
|
init_workspace();
|
|
11286
11342
|
init_workspace_ensure();
|
|
11287
11343
|
init_workspace_ownership();
|
|
@@ -11333,6 +11389,7 @@ async function environmentStop(repoPath, options = {}) {
|
|
|
11333
11389
|
const removedRoutes = removeHostRoutesWhere(
|
|
11334
11390
|
(route) => sameWorkspacePath(route.repoPath, repoPath)
|
|
11335
11391
|
);
|
|
11392
|
+
await ensureTraefikRoutesRemoved(removedRoutes);
|
|
11336
11393
|
return {
|
|
11337
11394
|
kind: linked ? "linked" : "primary",
|
|
11338
11395
|
repoPath,
|
|
@@ -11350,6 +11407,7 @@ var init_environment_stop = __esm({
|
|
|
11350
11407
|
init_devpod_mutation();
|
|
11351
11408
|
init_devpod_workspaces();
|
|
11352
11409
|
init_host_routes();
|
|
11410
|
+
init_traefik_route_health();
|
|
11353
11411
|
init_workspace();
|
|
11354
11412
|
init_workspace_lifecycle();
|
|
11355
11413
|
init_workspace_ownership();
|
|
@@ -15502,7 +15560,7 @@ var init_version = __esm({
|
|
|
15502
15560
|
|
|
15503
15561
|
// src/cli.ts
|
|
15504
15562
|
var import_commander = require("commander");
|
|
15505
|
-
var CLI_VERSION = true ? "0.0.
|
|
15563
|
+
var CLI_VERSION = true ? "0.0.50" : "0.0.0-dev";
|
|
15506
15564
|
var VERSION_FLAGS = /* @__PURE__ */ new Set(["-V", "--version"]);
|
|
15507
15565
|
function withErrorHandling(action2) {
|
|
15508
15566
|
return async (...args) => {
|
package/package.json
CHANGED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Upgrade to devrouter 0.0.49
|
|
2
|
+
|
|
3
|
+
Devrouter now checks a bounded 1,000-router view from both Traefik router APIs
|
|
4
|
+
when proving managed routes. This prevents Traefik's default 100-entry page
|
|
5
|
+
from hiding a valid route in a large shared installation.
|
|
6
|
+
|
|
7
|
+
1. Install `@devrouter/cli@0.0.49` on the host and bump `.devrouter.yml` to
|
|
8
|
+
`devrouter.version: 0.0.49`.
|
|
9
|
+
2. Keep using `devrouter ensure <checkout>`. Route naming, publication, and
|
|
10
|
+
restart-once recovery are unchanged.
|
|
11
|
+
3. If an expected router is absent from a full 1,000-entry API response,
|
|
12
|
+
Devrouter intentionally fails closed because the API view may be incomplete.
|
|
13
|
+
|
|
14
|
+
Verification:
|
|
15
|
+
|
|
16
|
+
- Run `devrouter -V --repo <checkout>` and confirm the installed CLI and local
|
|
17
|
+
repository report `0.0.49`.
|
|
18
|
+
- Start one safe representative managed profile with `devrouter ensure`, verify
|
|
19
|
+
its expected routes and zero profile drift, then stop that exact checkout and
|
|
20
|
+
require zero remaining routes.
|
|
21
|
+
|
|
22
|
+
Report template:
|
|
23
|
+
|
|
24
|
+
- CLI/config version: `0.0.49`
|
|
25
|
+
- Representative route proof and stop: `<passed, deferred, or details>`
|
|
26
|
+
- Remaining blockers: `<none or details>`
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Upgrade to devrouter 0.0.50
|
|
2
|
+
|
|
3
|
+
Devrouter now proves that Traefik unloaded exact HTTP and TCP routers removed
|
|
4
|
+
by stop, down, managed profile replacement, or rollback. A stale router gets
|
|
5
|
+
the same serialized Traefik-only restart used for publication recovery.
|
|
6
|
+
|
|
7
|
+
1. Install `@devrouter/cli@0.0.50` on the host and bump `.devrouter.yml` to
|
|
8
|
+
`devrouter.version: 0.0.50`.
|
|
9
|
+
2. Keep using the existing `devrouter ensure`, `devrouter stop`, and managed
|
|
10
|
+
profile commands. Route naming and configuration are unchanged.
|
|
11
|
+
3. Treat a cleanup error after one restart as fail-closed evidence that
|
|
12
|
+
Traefik's live router set did not match the canonical route generation.
|
|
13
|
+
|
|
14
|
+
Verification:
|
|
15
|
+
|
|
16
|
+
- Run `devrouter -V --repo <checkout>` and confirm the installed CLI and local
|
|
17
|
+
repository report `0.0.50`.
|
|
18
|
+
- Start one safe representative managed profile, verify its exact routes, then
|
|
19
|
+
stop that exact checkout and require both canonical route count and matching
|
|
20
|
+
Traefik API routers to be zero.
|
|
21
|
+
|
|
22
|
+
Report template:
|
|
23
|
+
|
|
24
|
+
- CLI/config version: `0.0.50`
|
|
25
|
+
- Representative route publication and removal proof: `<passed, deferred, or details>`
|
|
26
|
+
- Remaining blockers: `<none or details>`
|