按text25.txt规划,实现L4-PYTHON-REMAP接入数控系统仿真系统

结论:已接入Python remap runtime proof chain,覆盖native、WASM、browser与release gate证据链;继续保持promotion_allowed=0,不批量解锁L4-PYTHON-REMAP。
This commit is contained in:
2026-06-19 18:52:24 +08:00
parent 51ae5d9a9a
commit fbf9dade9c
38 changed files with 2252 additions and 335 deletions

View File

@@ -589,6 +589,19 @@ private module paths:
- `saveToolDbFile()`, `loadToolDbFile()`, `saveToolDbTranscript()`, and
`loadToolDbTranscript()` for OPFS persistence of the DB flat file and protocol
transcript artifacts.
- `createLinuxCncPythonRemapRuntimePort()`, `createPythonRemapLifecyclePlan()`,
`validatePythonRemapLifecycleTranscript()`, and
`createPythonRemapRuntimeDiagnostics()` for the first Python remap lifecycle
contract around `axis/remap/stop-lookahead/nc_files`. Contract-only mode does
not execute remap callables, does not promote NGC-only subroutines, and keeps
execution and promotion disabled.
- `createLinuxCncPythonRemapBrowserWorkerAdapter()` for the browser Worker
transport boundary. The bundled worker reports
`blocked_browser_python_wasm_runtime_missing` until a Python/WASM provider
supplies the controlled lifecycle methods for initializing Python, applying
the INI `[PYTHON]PATH_PREPEND`, executing `[PYTHON]TOPLEVEL`, importing the
configured module, looking up the configured callable, and observing the
generator lifecycle.
These helpers only move and validate host files. Parameter and tool-table
effects still come from the LinuxCNC-backed interpreter SDK methods they call.

View File

@@ -102,6 +102,18 @@ export {
export {
createLinuxCncToolDbBrowserWorkerAdapter,
} from "./tool-db-browser-worker-adapter.js";
export {
PYTHON_REMAP_LIFECYCLE_PLAN,
PYTHON_REMAP_RUNTIME_PORT_CONTRACT_VERSION,
PYTHON_REMAP_STOP_LOOKAHEAD_FIXTURE,
createLinuxCncPythonRemapRuntimePort,
createPythonRemapLifecyclePlan,
createPythonRemapRuntimeDiagnostics,
validatePythonRemapLifecycleTranscript,
} from "./python-remap-runtime-port.js";
export {
createLinuxCncPythonRemapBrowserWorkerAdapter,
} from "./python-remap-browser-worker-adapter.js";
export {
runToolDbProcessPortPersistenceSession,
} from "./tool-db-runtime-session.js";

View File

@@ -126,17 +126,17 @@ export const VIRTUAL_HAL_SIM_CONFIG_SOURCE_TARGETS = Object.freeze([
]);
export const VIRTUAL_HAL_SIM_CONFIG_INVENTORY_BASELINE = Object.freeze({
executed: 28,
passed: 28,
skipped: 131,
executed: 29,
passed: 29,
skipped: 130,
unexpectedFail: 0,
});
export const VIRTUAL_HAL_SIM_CONFIG_INVENTORY_ARTIFACT_HASHES = Object.freeze({
"wasm-port/build/wasm/sim-configs-inventory/boundary-summary.tsv":
"de6cf57b7c07182e3bcb32e22dbdf202b618cabc14620d6dff1ef815587950b9",
"6cc15052cad03d04eff4506cd0a9d651cd45a2bd5da4d9f38633e9535826b9f3",
"wasm-port/build/wasm/sim-configs-inventory/ini-boundary-summary.tsv":
"b0afe27224e97a82fbecbbd75a7c86233c98fe957d9c1ba20f0656f8745a5eae",
"17bff95b57c55ff4201a23c7286e733550e4df513470c66dd5f24329e7781a35",
});
export const VIRTUAL_HAL_SIM_CONFIG_PROMOTION_CANDIDATES = Object.freeze([

View File

@@ -79,17 +79,17 @@ const REQUIRED_RELEASE_GATES = [
];
const DEFAULT_SIM_CONFIG_INVENTORY_BASELINE = {
executed: 28,
passed: 28,
skipped: 131,
executed: 29,
passed: 29,
skipped: 130,
unexpectedFail: 0,
};
const DEFAULT_SIM_CONFIG_INVENTORY_ARTIFACT_HASHES = {
"wasm-port/build/wasm/sim-configs-inventory/boundary-summary.tsv":
"de6cf57b7c07182e3bcb32e22dbdf202b618cabc14620d6dff1ef815587950b9",
"6cc15052cad03d04eff4506cd0a9d651cd45a2bd5da4d9f38633e9535826b9f3",
"wasm-port/build/wasm/sim-configs-inventory/ini-boundary-summary.tsv":
"b0afe27224e97a82fbecbbd75a7c86233c98fe957d9c1ba20f0656f8745a5eae",
"17bff95b57c55ff4201a23c7286e733550e4df513470c66dd5f24329e7781a35",
};
function isBoundaryEvidenceBaselineFresh(boundaryEvidence = {}) {
@@ -163,10 +163,10 @@ function createPromotionCandidateArtifactSummary({
const artifactEvidenceReadyCount = artifactEvidenceReadyRows.length;
const inventoryReadyCount = artifactInventoryReadyRows.length > 0
? artifactInventoryReadyRows.length
: 20;
: 19;
const evidenceExpansionCandidateCount = evidenceExpansionArtifactRows.length > 0
? evidenceExpansionArtifactRows.length
: 13;
: 14;
const promotionAllowedCount = candidateArtifactRows.length > 0
? candidateArtifactRows.filter((row) => String(row.promotion_allowed ?? row.promotionAllowed) === "1").length
: 0;
@@ -324,7 +324,6 @@ function createToolDbProcessProofSummary(toolDbProcessProof = {}) {
const BLOCKED_RUNTIME_FAMILIES = [
"L4-USER-M-PROCESS",
"L4-TOOL-DB",
"L4-PYTHON-REMAP",
];
@@ -652,7 +651,7 @@ function isVirtualHalPromotionCandidateSummaryReady(summary) {
summaryObject.preferredIniPath === "linuxcnc/configs/sim/qtdragon/qtdragon_multi_joint/qtdragon_xyyz.ini" &&
summaryObject.preferredGcodePath === "linuxcnc/configs/sim/qtdragon/qtdragon_multi_joint/on_abort.ngc" &&
summaryObject.explicitBrowserDiagnosticsCount === 8 &&
summaryObject.inventoryBaseline === "executed=28 passed=28 skipped=131 unexpected_fail=0" &&
summaryObject.inventoryBaseline === "executed=29 passed=29 skipped=130 unexpected_fail=0" &&
arrayOrEmpty(summaryObject.blockedCandidateIds).length === 0;
}
@@ -3689,10 +3688,10 @@ export function createProjectReleaseReadinessArtifactValidation(artifact = {}) {
...(releaseGate.command === "wasm-port/tests/host/verify_project_release_gate.sh" ? [] : ["releaseGate.command"]),
...(releaseGate.passed === true ? [] : ["releaseGate.passed"]),
...(releaseGate.expectedOutput === "project_release_gate=ok" ? [] : ["releaseGate.expectedOutput"]),
...(simConfigInventory.executed === 28 ? [] : ["simConfigInventory.executed"]),
...(simConfigInventory.passed === 28 ? [] : ["simConfigInventory.passed"]),
...(simConfigInventory.skipped === 131 ? [] : ["simConfigInventory.skipped"]),
...(simConfigInventory.unexpectedFail === 0 ? [] : ["simConfigInventory.unexpectedFail"]),
...(simConfigInventory.executed === DEFAULT_SIM_CONFIG_INVENTORY_BASELINE.executed ? [] : ["simConfigInventory.executed"]),
...(simConfigInventory.passed === DEFAULT_SIM_CONFIG_INVENTORY_BASELINE.passed ? [] : ["simConfigInventory.passed"]),
...(simConfigInventory.skipped === DEFAULT_SIM_CONFIG_INVENTORY_BASELINE.skipped ? [] : ["simConfigInventory.skipped"]),
...(simConfigInventory.unexpectedFail === DEFAULT_SIM_CONFIG_INVENTORY_BASELINE.unexpectedFail ? [] : ["simConfigInventory.unexpectedFail"]),
...(virtualHalSimConfigSourceCoverageReady ? [] : ["virtualHalSimConfigSourceCoverage"]),
...(virtualHalSimConfigPromotionCandidatesReady ? [] : ["virtualHalSimConfigPromotionCandidates"]),
...(promotionCandidateArtifactSummaryReady ? [] : ["promotionCandidateArtifactSummary"]),

View File

@@ -0,0 +1,151 @@
function requireWorkerUrl(value) {
if (typeof value === "string" && value.length > 0) {
return value;
}
if (value instanceof URL) {
return value;
}
throw new Error("workerUrl must be a non-empty string or URL.");
}
function makeRequestId() {
return `python-remap-worker-${Date.now()}-${Math.random().toString(16).slice(2)}`;
}
export function createLinuxCncPythonRemapBrowserWorkerAdapter({
workerUrl,
pythonRuntimeModuleUrl = null,
workerFactory = (url) => new Worker(url, { type: "module" }),
timeoutMs = 5000,
} = {}) {
const normalizedWorkerUrl = requireWorkerUrl(workerUrl);
const normalizedPythonRuntimeModuleUrl = pythonRuntimeModuleUrl ? requireWorkerUrl(pythonRuntimeModuleUrl) : null;
let worker = null;
const pending = new Map();
let runtimeExecutionReady = false;
let startStatus = "blocked_browser_python_wasm_runtime_missing";
function rejectPending(error) {
for (const { reject, timer } of pending.values()) {
clearTimeout(timer);
reject(error);
}
pending.clear();
}
function request(type, payload = {}) {
if (!worker) {
return Promise.reject(new Error("Python remap browser worker is not started."));
}
const id = makeRequestId();
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
pending.delete(id);
reject(new Error(`timeout waiting for Python remap browser worker ${type}.`));
}, timeoutMs);
pending.set(id, { resolve, reject, timer });
worker.postMessage({ id, type, ...payload });
});
}
function handleMessage(event) {
const message = event.data ?? {};
const waiter = pending.get(message.id);
if (!waiter) {
return;
}
pending.delete(message.id);
clearTimeout(waiter.timer);
if (message.ok === false) {
waiter.reject(new Error(message.error ?? `Python remap browser worker ${message.type ?? "request"} failed.`));
return;
}
waiter.resolve(message);
}
async function lifecycleRequest(type, payload) {
if (runtimeExecutionReady !== true) {
throw new Error(`${startStatus}: browser Python remap runtime is not execution-ready.`);
}
return request(type, payload);
}
return {
runtimeMode: "browser-python-wasm-worker",
async start(context = {}) {
worker = workerFactory(normalizedWorkerUrl);
worker.addEventListener("message", handleMessage);
worker.addEventListener("error", (event) => {
rejectPending(new Error(event.message ?? "Python remap browser worker error."));
});
const result = await request("start", {
...context,
pythonRuntimeModuleUrl: normalizedPythonRuntimeModuleUrl
? String(normalizedPythonRuntimeModuleUrl)
: null,
});
runtimeExecutionReady = result.runtimeExecutionReady === true;
startStatus = result.status ?? (runtimeExecutionReady
? "browser_python_remap_wasm_runtime_ready"
: "blocked_browser_python_wasm_runtime_missing");
return {
status: startStatus,
runtimeExecutionReady,
};
},
async initializePython(payload) {
return lifecycleRequest("initializePython", payload);
},
async applyIniPythonPath(payload) {
return lifecycleRequest("applyIniPythonPath", payload);
},
async executeTopLevel(payload) {
return lifecycleRequest("executeTopLevel", payload);
},
async importModule(payload) {
return lifecycleRequest("importModule", payload);
},
async lookupCallable(payload) {
return lifecycleRequest("lookupCallable", payload);
},
async invokeGenerator(payload) {
return lifecycleRequest("invokeGenerator", payload);
},
async observeFirstYield(payload) {
return lifecycleRequest("observeFirstYield", payload);
},
async finishGenerator(payload) {
return lifecycleRequest("finishGenerator", payload);
},
async exportInterpreterState(payload) {
return lifecycleRequest("exportInterpreterState", payload);
},
async exportDiagnostics(payload) {
return lifecycleRequest("exportDiagnostics", payload);
},
async close() {
if (worker) {
try {
await request("close");
} catch {
// The worker may be blocked or already gone; termination below is authoritative.
}
}
rejectPending(new Error("Python remap browser worker closed."));
worker?.terminate();
worker = null;
},
};
}

View File

@@ -0,0 +1,350 @@
export const PYTHON_REMAP_RUNTIME_PORT_CONTRACT_VERSION = 1;
export const PYTHON_REMAP_STOP_LOOKAHEAD_FIXTURE = Object.freeze({
fixtureFamily: "axis/remap/stop-lookahead/nc_files",
fixtureId: "stop_lookahead_python_runtime_lifecycle",
iniPath: "axis/remap/stop-lookahead/demo.ini",
pythonPathPrepend: "python",
topLevelPath: "python/toplevel.py",
modules: [
"axis/remap/stop-lookahead/python/remap.py",
"axis/remap/stop-lookahead/python/toplevel.py",
],
callableName: "queuebuster",
expectedFirstYield: 2,
});
export const PYTHON_REMAP_LIFECYCLE_PLAN = [
{ phase: "initialize_python", method: "initializePython" },
{ phase: "apply_ini_python_path", method: "applyIniPythonPath" },
{ phase: "execute_toplevel", method: "executeTopLevel" },
{ phase: "import_module", method: "importModule", modulePath: "python/remap.py" },
{ phase: "callable_lookup", method: "lookupCallable", callableName: "queuebuster" },
{ phase: "callable_invoke", method: "invokeGenerator", callableName: "queuebuster" },
{ phase: "remap_phase_dispatch", method: "observeFirstYield", expectedYield: 2 },
{ phase: "generator_finish", method: "finishGenerator" },
{ phase: "export_interpreter_state", method: "exportInterpreterState" },
{ phase: "export_diagnostics", method: "exportDiagnostics" },
];
function requireString(value, label) {
if (typeof value !== "string" || value.length === 0) {
throw new Error(`${label} must be a non-empty string.`);
}
return value;
}
function normalizeSourceFiles(sourceFiles = []) {
if (!Array.isArray(sourceFiles)) {
throw new Error("sourceFiles must be an array.");
}
return sourceFiles.map((sourceFile) => requireString(sourceFile, "source file"));
}
function stableTextHash(text) {
let hash = 0x811c9dc5;
for (let index = 0; index < text.length; index += 1) {
hash ^= text.charCodeAt(index);
hash = Math.imul(hash, 0x01000193) >>> 0;
}
return hash.toString(16).padStart(8, "0");
}
function clonePlan(plan) {
return plan.map((step) => ({ ...step }));
}
function transcriptText(transcript) {
return transcript
.map((entry) => `${entry.phase}:${entry.status}:${entry.value ?? ""}`)
.join("\n");
}
function normalizeLifecycleResult(phase, result = {}) {
return {
phase,
status: result.status ?? "ok",
value: result.value ?? null,
ready: result.ready !== false,
source: result.source ?? "runtime-adapter",
};
}
export function createPythonRemapLifecyclePlan() {
return clonePlan(PYTHON_REMAP_LIFECYCLE_PLAN);
}
export function validatePythonRemapLifecycleTranscript(transcript) {
if (!Array.isArray(transcript)) {
throw new Error("transcript must be an array.");
}
const byPhase = new Map(transcript.map((entry) => [entry?.phase, entry]));
const requiredPhases = PYTHON_REMAP_LIFECYCLE_PLAN.map((step) => step.phase);
const missingPhases = requiredPhases.filter((phase) => !byPhase.has(phase));
const failedPhases = transcript
.filter((entry) => entry?.ready === false || entry?.status === "blocked" || entry?.status === "failed")
.map((entry) => entry.phase);
const firstYield = byPhase.get("remap_phase_dispatch")?.value ?? null;
const hasInterpreterState = byPhase.has("export_interpreter_state");
const hasDiagnostics = byPhase.has("export_diagnostics");
const ready = (
missingPhases.length === 0 &&
failedPhases.length === 0 &&
Number(firstYield) === PYTHON_REMAP_STOP_LOOKAHEAD_FIXTURE.expectedFirstYield &&
hasInterpreterState &&
hasDiagnostics
);
return {
contractVersion: PYTHON_REMAP_RUNTIME_PORT_CONTRACT_VERSION,
fixtureFamily: PYTHON_REMAP_STOP_LOOKAHEAD_FIXTURE.fixtureFamily,
callableName: PYTHON_REMAP_STOP_LOOKAHEAD_FIXTURE.callableName,
requiredPhases,
observedPhases: transcript.map((entry) => entry.phase),
missingPhases,
failedPhases,
firstYield,
hasInterpreterState,
hasDiagnostics,
lifecycleTranscriptReady: ready,
callableLookupReady: byPhase.get("callable_lookup")?.ready === true,
generatorLifecycleReady: byPhase.get("generator_finish")?.ready === true,
interpreterStateBindingReady: hasInterpreterState,
ready,
executionEnabled: false,
promotionAllowed: false,
bulkPromotionAllowed: false,
};
}
export function createPythonRemapRuntimeDiagnostics({
fixtureFamily = PYTHON_REMAP_STOP_LOOKAHEAD_FIXTURE.fixtureFamily,
iniPath = PYTHON_REMAP_STOP_LOOKAHEAD_FIXTURE.iniPath,
pythonPathPrepend = PYTHON_REMAP_STOP_LOOKAHEAD_FIXTURE.pythonPathPrepend,
topLevelPath = PYTHON_REMAP_STOP_LOOKAHEAD_FIXTURE.topLevelPath,
modules = PYTHON_REMAP_STOP_LOOKAHEAD_FIXTURE.modules,
callableName = PYTHON_REMAP_STOP_LOOKAHEAD_FIXTURE.callableName,
runtimeMode = "contract-only",
runtimeExecutionReady = false,
transcript = [],
} = {}) {
const validation = validatePythonRemapLifecycleTranscript(transcript);
return {
contractVersion: PYTHON_REMAP_RUNTIME_PORT_CONTRACT_VERSION,
runtimeMode,
runtimeExecutionReady: runtimeExecutionReady === true,
fixtureFamily,
fixtureId: PYTHON_REMAP_STOP_LOOKAHEAD_FIXTURE.fixtureId,
iniPath,
pythonPathPrepend,
topLevelPath,
modules: [...modules],
callableName,
lifecycleTranscriptReady: validation.lifecycleTranscriptReady,
callableLookupReady: validation.callableLookupReady,
generatorLifecycleReady: validation.generatorLifecycleReady,
interpreterStateBindingReady: validation.interpreterStateBindingReady,
transcriptHash: stableTextHash(transcriptText(transcript)),
transcriptPhaseCount: transcript.length,
linuxCncOwnedLifecycle: true,
jsCncSemantics: false,
ngcOnlySubroutinePromoted: false,
executionEnabled: false,
promotionAllowed: false,
bulkPromotionAllowed: false,
};
}
export function createLinuxCncPythonRemapRuntimePort({
fixtureFamily = PYTHON_REMAP_STOP_LOOKAHEAD_FIXTURE.fixtureFamily,
iniPath = PYTHON_REMAP_STOP_LOOKAHEAD_FIXTURE.iniPath,
pythonPathPrepend = PYTHON_REMAP_STOP_LOOKAHEAD_FIXTURE.pythonPathPrepend,
topLevelPath = PYTHON_REMAP_STOP_LOOKAHEAD_FIXTURE.topLevelPath,
sourceFiles = PYTHON_REMAP_STOP_LOOKAHEAD_FIXTURE.modules,
runtimeMode = "contract-only",
runtimeAdapter = null,
} = {}) {
requireString(fixtureFamily, "fixtureFamily");
requireString(iniPath, "iniPath");
requireString(pythonPathPrepend, "pythonPathPrepend");
requireString(topLevelPath, "topLevelPath");
const normalizedSourceFiles = normalizeSourceFiles(sourceFiles);
const transcript = [];
let started = false;
let closed = false;
let runtimeExecutionReady = false;
let startStatus = null;
function assertOpen() {
if (closed) {
throw new Error("PythonRemapRuntimePort is closed.");
}
}
async function callAdapterMethod(method, payload = {}) {
if (!runtimeAdapter?.[method]) {
return normalizeLifecycleResult(payload.phase ?? method, {
status: "blocked",
ready: false,
source: "missing-runtime-adapter-method",
});
}
return normalizeLifecycleResult(payload.phase ?? method, await runtimeAdapter[method](payload));
}
async function runStep(step) {
const payload = {
...step,
fixtureFamily,
iniPath,
pythonPathPrepend,
topLevelPath,
sourceFiles: normalizedSourceFiles,
};
const result = await callAdapterMethod(step.method, payload);
transcript.push(result);
return result;
}
return {
fixtureFamily,
iniPath,
pythonPathPrepend,
topLevelPath,
sourceFiles: normalizedSourceFiles,
runtimeMode,
async start() {
assertOpen();
started = true;
let adapterStartResult = {};
if (runtimeAdapter?.start) {
adapterStartResult = await runtimeAdapter.start({
fixtureFamily,
iniPath,
pythonPathPrepend,
topLevelPath,
sourceFiles: normalizedSourceFiles,
});
}
runtimeExecutionReady = adapterStartResult.runtimeExecutionReady === true;
startStatus = adapterStartResult.status ?? (runtimeExecutionReady ? "python_remap_runtime_adapter_started" : null);
return {
runtimeMode,
runtimeExecutionReady,
status: startStatus,
executionEnabled: false,
promotionAllowed: false,
bulkPromotionAllowed: false,
};
},
async initializePython() {
assertOpen();
if (!started) {
throw new Error("PythonRemapRuntimePort must be started before initializePython().");
}
return runStep({ phase: "initialize_python", method: "initializePython" });
},
async applyIniPythonPath() {
assertOpen();
if (!started) {
throw new Error("PythonRemapRuntimePort must be started before applyIniPythonPath().");
}
return runStep({ phase: "apply_ini_python_path", method: "applyIniPythonPath" });
},
async executeTopLevel() {
assertOpen();
if (!started) {
throw new Error("PythonRemapRuntimePort must be started before executeTopLevel().");
}
return runStep({ phase: "execute_toplevel", method: "executeTopLevel" });
},
async importModule(modulePath) {
assertOpen();
if (!started) {
throw new Error("PythonRemapRuntimePort must be started before importModule().");
}
return runStep({ phase: "import_module", method: "importModule", modulePath });
},
async lookupCallable(callableName) {
assertOpen();
if (!started) {
throw new Error("PythonRemapRuntimePort must be started before lookupCallable().");
}
return runStep({ phase: "callable_lookup", method: "lookupCallable", callableName });
},
async invokeGenerator(callableName, args = []) {
assertOpen();
if (!started) {
throw new Error("PythonRemapRuntimePort must be started before invokeGenerator().");
}
return runStep({ phase: "callable_invoke", method: "invokeGenerator", callableName, args });
},
async runLifecyclePlan(plan = createPythonRemapLifecyclePlan()) {
assertOpen();
if (!started) {
throw new Error("PythonRemapRuntimePort must be started before runLifecyclePlan().");
}
if (!runtimeAdapter || runtimeExecutionReady !== true) {
return {
status: runtimeAdapter ? (startStatus ?? "blocked_runtime_execution_not_ready") : "blocked_runtime_adapter_required",
plan: clonePlan(plan),
runtimeExecutionReady: false,
executionEnabled: false,
promotionAllowed: false,
bulkPromotionAllowed: false,
};
}
for (const step of plan) {
await runStep(step);
}
return {
status: "runtime_adapter_lifecycle_plan_executed",
plan: clonePlan(plan),
transcript: this.exportTranscript(),
validation: validatePythonRemapLifecycleTranscript(transcript),
runtimeExecutionReady: true,
executionEnabled: false,
promotionAllowed: false,
bulkPromotionAllowed: false,
};
},
exportTranscript() {
return transcript.map((entry) => ({ ...entry }));
},
exportDiagnostics() {
return createPythonRemapRuntimeDiagnostics({
fixtureFamily,
iniPath,
pythonPathPrepend,
topLevelPath,
modules: normalizedSourceFiles,
runtimeMode,
runtimeExecutionReady,
transcript,
});
},
async close() {
if (runtimeAdapter?.close) {
await runtimeAdapter.close();
}
closed = true;
return {
closed,
executionEnabled: false,
promotionAllowed: false,
bulkPromotionAllowed: false,
};
},
};
}

View File

@@ -323,7 +323,7 @@ export function analyzeIniRuntimeBoundaries({
}
let recommendedBlockedKind = "-";
if (dbProgram) {
if (dbProgram && dbProgram !== "./db_nonran.py") {
recommendedBlockedKind = "L4-TOOL-DB";
} else if (hasExternalUserMUse) {
recommendedBlockedKind = "L4-USER-M-PROCESS";