@merchantduo/code 0.3.0-beta.1 → 0.3.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/app/system-prompt.js +6 -2
- package/dist/environments/adapters/warden.d.ts +7 -1
- package/dist/environments/adapters/warden.js +24 -3
- package/dist/integrations/pi/permissions.d.ts +1 -0
- package/dist/integrations/pi/permissions.js +10 -3
- package/dist/integrations/pi/workspace.d.ts +1 -0
- package/dist/integrations/pi/workspace.js +36 -6
- package/dist/workflows/post-edit.d.ts +7 -0
- package/dist/workflows/post-edit.js +51 -0
- package/package.json +1 -1
- package/skills/magento-2.4/SKILL.md +241 -2
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@ environments:
|
|
|
48
48
|
root: /var/www/html
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
Local uses the checkout directly. Warden routes project and Magento execution through its configured service. Local and Warden sessions start in `normal`; SSH starts in `read-only`.
|
|
51
|
+
Local uses the checkout directly. Warden routes project and Magento execution through its configured service. Local and Warden sessions start in `normal`; SSH starts in `read-only`. Run `/duo-switch-permissions` to choose a mode interactively, or provide `<read-only|normal|yolo>` directly to change only the current session; `merchantduo --yolo` starts in `yolo`. Configure SSH user, port, and key through an OpenSSH host alias:
|
|
52
52
|
|
|
53
53
|
```sshconfig
|
|
54
54
|
Host shop-stage
|
|
@@ -2,8 +2,12 @@ import { packagePath } from "#shared/package-paths";
|
|
|
2
2
|
export function mage2genRoot() {
|
|
3
3
|
return packagePath("vendor/mage2gen");
|
|
4
4
|
}
|
|
5
|
+
// Source: MAGENTO_SYSTEM_PROMPT.md. Keep this contract verbatim in every Magento session.
|
|
6
|
+
const generalMagentoEngineeringPrompt = "# Magento Engineering\n\nFor Magento Open Source / Adobe Commerce work, optimize for the **smallest Magento-native, maintainable, upgrade-safe change**, not merely a working PHP implementation.\n\nBefore version-sensitive decisions, inspect the actual project when possible: Magento edition/version, `composer.lock`, PHP version, installed modules, theme, Hyvä packages, and relevant third-party versions. Never assume Luma, Hyvä, Commerce features, or extension APIs are present.\n\nPrefer documented Magento mechanisms over broad overrides or custom infrastructure. Prefer composition over inheritance. Treat non-`@api` Magento/vendor internals as upgrade-sensitive.\n\nDo not blindly apply folklore such as \"always use repositories\", \"never use preferences\", or \"plugins are always better\". Choose mechanisms according to their intended scope and blast radius.\n\nDefault engineering expectations:\n\n- constructor DI for normal dependencies; no direct `ObjectManager` in feature/business code;\n- declarative XML/configuration before runtime PHP when Magento provides the capability;\n- narrow plugins/events/composition before broad class replacement;\n- business logic in reusable services, not controllers/plugins/observers/resolvers/templates;\n- explicit request/customer/store/quote context passed into services instead of deep session/request dependencies;\n- batch-oriented data access; avoid N+1 and repository/entity loads in large loops;\n- respect FPC, cache identities, indexers/MView, retries, concurrency, ACL, ownership, escaping, CSRF, and production compilation;\n- never edit `vendor/` directly.\n\nFor non-trivial Magento architecture, implementation, extension, review, or refactoring tasks, load the `magento-extension-best-practices` skill before deciding the implementation approach. This especially applies to DI, plugins/preferences/events, service contracts and persistence, schema/XML, cache/indexers/queues, Admin UI, Luma/Hyvä frontend, REST/GraphQL, security, and testing.";
|
|
5
7
|
export function merchantDuoSystemPrompt(operatorRole, mage2genPath = mage2genRoot()) {
|
|
6
|
-
return
|
|
8
|
+
return `${generalMagentoEngineeringPrompt}
|
|
9
|
+
|
|
10
|
+
You are MerchantDuo, a senior Magento 2.4 architect and implementation partner. Work directly in the selected MerchantDuo environment and theme scope. Before changing code, inspect the relevant module, theme inheritance chain, configuration, and established local conventions. Make focused, production-quality changes; do not preserve obsolete compatibility paths or add speculative abstractions.
|
|
7
11
|
|
|
8
12
|
${operatorRole ? `The operator is a ${operatorRole.replace(/-/g, " ")}. Use this only as context for framing explanations and recommendations; it does not limit available tools, permissions, or supported work.
|
|
9
13
|
|
|
@@ -21,5 +25,5 @@ Session permission mode is shown in the startup context and can be changed only
|
|
|
21
25
|
|
|
22
26
|
For a supported cache, deploy, indexing, or test operation, prefer \`magento_workflow\`. For a different n98-magerun2 subcommand, use \`magerun2\` with an exact \`args: string[]\` vector, never a shell command or interpolation. When the user did not directly request execution, return its preview without \`execute: true\`; an executed generic magerun2 call requires \`execute: true\` and uses the same sole harness confirmation. Never send \`dev:console\` through \`magerun2\`: use the typed \`magento_php_repl\` tool instead.
|
|
23
27
|
|
|
24
|
-
Environment lifecycle is explicit. Warden has known direct controls. SSH is always reported running and must never be started, stopped, or restarted. A local environment is agent-directed: when the user requests a start or stop, inspect the actual project lifecycle first, then call \`environment_start\` or \`environment_stop\` with the exact smallest command. Never guess a generic Docker, Compose, npm, or service command, and never affect unrelated host services. Use \`environment_set_status\` after a status inspection that does not itself start or stop the stack. Never inspect app/etc/env.php or credentials.`;
|
|
28
|
+
Environment lifecycle is explicit. Warden has known direct controls. SSH is always reported running and must never be started, stopped, or restarted. A local environment is agent-directed: when the user requests a start or stop, inspect the actual project lifecycle first, then call \`environment_start\` or \`environment_stop\` with the exact smallest command. Never guess a generic Docker, Compose, npm, or service command, and never affect unrelated host services. Use \`environment_set_status\` after a status inspection that does not itself start or stop the stack. In Warden sessions, use container paths under the configured root, normally \`/var/www/html\`; if a tool maps a host path, follow its warning. Successful writes and edits return post-edit hints. Fix any reported Magento XML XSD validation warning before moving on. Never inspect app/etc/env.php or credentials.`;
|
|
25
29
|
}
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import type { EnvironmentAdapter } from "#environments/adapter";
|
|
2
2
|
import type { WardenEnvironment } from "#environments/model";
|
|
3
3
|
export declare class WardenAdapter implements EnvironmentAdapter {
|
|
4
|
+
#private;
|
|
4
5
|
readonly environment: WardenEnvironment;
|
|
5
6
|
readonly cwd: string;
|
|
6
7
|
constructor(environment: WardenEnvironment, cwd: string);
|
|
7
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Resolve a path into the Warden container before a routed command runs.
|
|
10
|
+
* Relative paths are container-relative; absolute paths may be either
|
|
11
|
+
* canonical container paths or host paths below the Warden project root.
|
|
12
|
+
*/
|
|
13
|
+
path(candidate: string): string;
|
|
8
14
|
command(argv: string[], target?: string): {
|
|
9
15
|
file: string;
|
|
10
16
|
args: string[];
|
|
@@ -1,13 +1,30 @@
|
|
|
1
|
-
import { resolve } from "node:path";
|
|
1
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
2
|
+
import { contain } from "#environments/path-policy";
|
|
2
3
|
export class WardenAdapter {
|
|
3
4
|
environment;
|
|
4
5
|
cwd;
|
|
6
|
+
#hostRoot;
|
|
7
|
+
#containerRoot;
|
|
5
8
|
constructor(environment, cwd) {
|
|
6
9
|
this.environment = environment;
|
|
7
10
|
this.cwd = cwd;
|
|
11
|
+
this.#hostRoot = resolve(cwd, environment.projectRoot);
|
|
12
|
+
this.#containerRoot = resolve(environment.root);
|
|
8
13
|
}
|
|
9
|
-
|
|
10
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Resolve a path into the Warden container before a routed command runs.
|
|
16
|
+
* Relative paths are container-relative; absolute paths may be either
|
|
17
|
+
* canonical container paths or host paths below the Warden project root.
|
|
18
|
+
*/
|
|
19
|
+
path(candidate) {
|
|
20
|
+
if (!isAbsolute(candidate))
|
|
21
|
+
return contain(this.#containerRoot, candidate);
|
|
22
|
+
const absolute = resolve(candidate);
|
|
23
|
+
if (within(this.#containerRoot, absolute))
|
|
24
|
+
return absolute;
|
|
25
|
+
if (within(this.#hostRoot, absolute))
|
|
26
|
+
return contain(this.#containerRoot, relative(this.#hostRoot, absolute));
|
|
27
|
+
throw new Error(`Path is outside the Warden project and container roots: ${candidate}`);
|
|
11
28
|
}
|
|
12
29
|
command(argv, target) {
|
|
13
30
|
const service = target
|
|
@@ -29,3 +46,7 @@ export class WardenAdapter {
|
|
|
29
46
|
return { file: "warden", args: actionArgs, cwd: resolve(this.cwd, this.environment.projectRoot) };
|
|
30
47
|
}
|
|
31
48
|
}
|
|
49
|
+
function within(root, candidate) {
|
|
50
|
+
const value = relative(root, candidate);
|
|
51
|
+
return value === "" || (value !== ".." && !value.startsWith(`..${sep}`));
|
|
52
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { type PermissionMode } from "#app/permission-mode";
|
|
3
3
|
import type { Environment } from "#environments/model";
|
|
4
|
+
export declare function permissionChoices(): PermissionMode[];
|
|
4
5
|
export declare function requiresExecuteConfirmation(toolName: string, input: Record<string, unknown>): boolean;
|
|
5
6
|
/** Return block/confirmation behavior; callers retain all hard capability constraints. */
|
|
6
7
|
export declare function permissionDecision(mode: PermissionMode, environment: Environment, toolName: string, input: Record<string, unknown>): "allow" | "block" | "confirm";
|
|
@@ -3,6 +3,9 @@ import { refreshStatus } from "#app/status";
|
|
|
3
3
|
import { runtime } from "#integrations/pi/session";
|
|
4
4
|
const workspaceMutations = new Set(["write", "edit"]);
|
|
5
5
|
const stoppedAllowed = new Set(["environment_status", "environment_start", "environment_stop", "environment_set_status"]);
|
|
6
|
+
export function permissionChoices() {
|
|
7
|
+
return [...permissionModes];
|
|
8
|
+
}
|
|
6
9
|
export function requiresExecuteConfirmation(toolName, input) {
|
|
7
10
|
return ["environment_start", "environment_stop"].includes(toolName) || ((toolName === "mage2gen_generate_module" || toolName === "magento_workflow" || toolName === "magerun2") && input.execute === true);
|
|
8
11
|
}
|
|
@@ -26,9 +29,13 @@ async function confirm(ctx, title, message) {
|
|
|
26
29
|
/** The sole MerchantDuo confirmation gate. YOLO removes dialogs, never hard constraints. */
|
|
27
30
|
export default function permissions(pi) {
|
|
28
31
|
pi.registerCommand("duo-switch-permissions", { description: "Set this session's permission mode: read-only, normal, or yolo.", handler: async (args, ctx) => {
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
const choices = permissionChoices();
|
|
33
|
+
const typed = args.trim();
|
|
34
|
+
const mode = typed || (ctx.hasUI ? await ctx.ui.select("Select session permission mode", choices) : undefined);
|
|
35
|
+
if (!mode)
|
|
36
|
+
return;
|
|
37
|
+
if (!choices.includes(mode)) {
|
|
38
|
+
ctx.ui.notify(`Choose one of: ${choices.join(", ")}`, "error");
|
|
32
39
|
return;
|
|
33
40
|
}
|
|
34
41
|
await runtime.boot(ctx.cwd);
|
|
@@ -16,6 +16,7 @@ type SearchParameters = {
|
|
|
16
16
|
*/
|
|
17
17
|
export declare function searchCommand(params: SearchParameters, path: string): string;
|
|
18
18
|
export declare function workspacePath(backend: EnvironmentBackend, cwd: string, candidate: string): string;
|
|
19
|
+
export declare function wardenHostPathWarning(backend: EnvironmentBackend, cwd: string, candidate: string): string | undefined;
|
|
19
20
|
/** Return only a declared MerchantDuo skill asset; project files remain environment-routed. */
|
|
20
21
|
export declare function packagedSkillPath(candidate: string): string | undefined;
|
|
21
22
|
export {};
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { access as hostAccess, readFile as hostReadFile } from "node:fs/promises";
|
|
2
|
-
import { relative, resolve } from "node:path";
|
|
2
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
3
3
|
import { Type } from "typebox";
|
|
4
4
|
import { createEditToolDefinition, createReadToolDefinition, createWriteToolDefinition, } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { runtime } from "#integrations/pi/session";
|
|
6
6
|
import { packagePath } from "#shared/package-paths";
|
|
7
|
+
import { postEditHints, validateMagentoXml } from "#workflows/post-edit";
|
|
7
8
|
const shell = (value) => `'${value.replace(/'/g, "'\\''")}'`;
|
|
8
9
|
const result = (value) => ({ content: [{ type: "text", text: value }], details: {} });
|
|
9
10
|
const packageSkillRoots = [
|
|
@@ -34,9 +35,19 @@ function registerFileTools(pi) {
|
|
|
34
35
|
const localRead = createReadToolDefinition(process.cwd());
|
|
35
36
|
const localWrite = createWriteToolDefinition(process.cwd());
|
|
36
37
|
const localEdit = createEditToolDefinition(process.cwd());
|
|
37
|
-
pi.registerTool({ ...localRead, async execute(id, params, signal, update, ctx) { const state = await runtime.boot(ctx.cwd);
|
|
38
|
-
pi.registerTool({ ...localWrite, async execute(id, params, signal, update, ctx) { const state = await runtime.boot(ctx.cwd);
|
|
39
|
-
pi.registerTool({ ...localEdit, async execute(id, params, signal, update, ctx) { const state = await runtime.boot(ctx.cwd);
|
|
38
|
+
pi.registerTool({ ...localRead, async execute(id, params, signal, update, ctx) { const state = await runtime.boot(ctx.cwd); const response = await createReadToolDefinition(ctx.cwd, { operations: fileOps(state.backend, ctx.cwd) }).execute(id, params, signal, update, ctx); return appendNotice(response, wardenHostPathWarning(state.backend, ctx.cwd, params.path)); } });
|
|
39
|
+
pi.registerTool({ ...localWrite, async execute(id, params, signal, update, ctx) { const state = await runtime.boot(ctx.cwd); const response = await createWriteToolDefinition(ctx.cwd, { operations: fileOps(state.backend, ctx.cwd) }).execute(id, params, signal, update, ctx); return appendPostEdit(response, state.backend, ctx.cwd, params.path, signal); } });
|
|
40
|
+
pi.registerTool({ ...localEdit, async execute(id, params, signal, update, ctx) { const state = await runtime.boot(ctx.cwd); const response = await createEditToolDefinition(ctx.cwd, { operations: fileOps(state.backend, ctx.cwd) }).execute(id, params, signal, update, ctx); return appendPostEdit(response, state.backend, ctx.cwd, params.path, signal); } });
|
|
41
|
+
}
|
|
42
|
+
async function appendPostEdit(response, backend, cwd, path, signal) {
|
|
43
|
+
const target = workspacePath(backend, cwd, path);
|
|
44
|
+
const validation = await validateMagentoXml(backend, target, signal);
|
|
45
|
+
return appendNotice(response, [wardenHostPathWarning(backend, cwd, path), ...postEditHints(path), validation].filter((value) => Boolean(value)).join("\n"));
|
|
46
|
+
}
|
|
47
|
+
function appendNotice(response, notice) {
|
|
48
|
+
if (!notice)
|
|
49
|
+
return response;
|
|
50
|
+
return { ...response, content: [...response.content, { type: "text", text: `MerchantDuo: ${notice}` }] };
|
|
40
51
|
}
|
|
41
52
|
function fileOps(backend, cwd) {
|
|
42
53
|
const readFile = async (path) => {
|
|
@@ -58,8 +69,27 @@ function fileOps(backend, cwd) {
|
|
|
58
69
|
throw new Error("Path is inaccessible"); }, mkdir: async (path) => { const response = await backend.run(["mkdir", "-p", workspacePath(backend, cwd, path)]); if (response.exitCode)
|
|
59
70
|
throw new Error(response.stderr || "Cannot create directory"); } };
|
|
60
71
|
}
|
|
61
|
-
export function workspacePath(backend, cwd, candidate) {
|
|
62
|
-
|
|
72
|
+
export function workspacePath(backend, cwd, candidate) {
|
|
73
|
+
if (backend.environment.type === "warden" && isAbsolute(candidate)) {
|
|
74
|
+
const containerPath = resolve(candidate);
|
|
75
|
+
if (contains(resolve(backend.environment.root), containerPath))
|
|
76
|
+
return containerPath;
|
|
77
|
+
}
|
|
78
|
+
const full = resolve(cwd, candidate);
|
|
79
|
+
const value = relative(resolve(cwd), full);
|
|
80
|
+
if (value === ".." || value.startsWith("../"))
|
|
81
|
+
throw new Error(`Path escapes workspace: ${candidate}`);
|
|
82
|
+
return backend.path(value);
|
|
83
|
+
}
|
|
84
|
+
export function wardenHostPathWarning(backend, cwd, candidate) {
|
|
85
|
+
if (backend.environment.type !== "warden" || !isAbsolute(candidate))
|
|
86
|
+
return undefined;
|
|
87
|
+
const hostPath = resolve(candidate);
|
|
88
|
+
if (!contains(resolve(cwd), hostPath))
|
|
89
|
+
return undefined;
|
|
90
|
+
const value = relative(resolve(cwd), hostPath);
|
|
91
|
+
return `Warden path warning: ${candidate} was mapped to ${backend.path(value)}. This session runs inside Warden; use the container path next time.`;
|
|
92
|
+
}
|
|
63
93
|
/** Return only a declared MerchantDuo skill asset; project files remain environment-routed. */
|
|
64
94
|
export function packagedSkillPath(candidate) {
|
|
65
95
|
const full = resolve(candidate);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { EnvironmentBackend } from "#environments/backend";
|
|
2
|
+
/** Keep immediate post-edit guidance narrow; operational work remains an explicit request. */
|
|
3
|
+
export declare function postEditHints(path: string): string[];
|
|
4
|
+
export declare function isMagentoXml(path: string): boolean;
|
|
5
|
+
/** Validate changed Magento XML in its own selected environment using Magento's URN resolver. */
|
|
6
|
+
export declare function validateMagentoXml(backend: EnvironmentBackend, path: string, signal?: AbortSignal): Promise<string>;
|
|
7
|
+
export declare function xmlValidationCommand(path: string): string[];
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const xsdValidator = String.raw `
|
|
2
|
+
$file = $argv[1] ?? '';
|
|
3
|
+
libxml_use_internal_errors(true);
|
|
4
|
+
$dom = new DOMDocument();
|
|
5
|
+
if (!$dom->load($file)) {
|
|
6
|
+
foreach (libxml_get_errors() as $error) fwrite(STDERR, trim($error->message) . " at line " . $error->line . "\n");
|
|
7
|
+
exit(2);
|
|
8
|
+
}
|
|
9
|
+
$root = $dom->documentElement;
|
|
10
|
+
$schema = $root ? $root->getAttributeNS('http://www.w3.org/2001/XMLSchema-instance', 'noNamespaceSchemaLocation') : '';
|
|
11
|
+
if (!$schema || strpos($schema, 'urn:magento:') !== 0) {
|
|
12
|
+
fwrite(STDERR, "Magento XML must declare xsi:noNamespaceSchemaLocation with a urn:magento schema.\n");
|
|
13
|
+
exit(2);
|
|
14
|
+
}
|
|
15
|
+
require getcwd() . '/app/bootstrap.php';
|
|
16
|
+
$errors = Magento\Framework\Config\Dom::validateDomDocument($dom, $schema);
|
|
17
|
+
if ($errors) {
|
|
18
|
+
foreach ($errors as $error) fwrite(STDERR, (string)$error . "\n");
|
|
19
|
+
exit(2);
|
|
20
|
+
}
|
|
21
|
+
echo "Magento XML XSD validation passed.\n";
|
|
22
|
+
`;
|
|
23
|
+
/** Keep immediate post-edit guidance narrow; operational work remains an explicit request. */
|
|
24
|
+
export function postEditHints(path) {
|
|
25
|
+
const value = path.toLowerCase();
|
|
26
|
+
const hints = ["Inspect the changed file and its closest Magento precedent before continuing."];
|
|
27
|
+
if (value.endsWith(".php") || value.endsWith(".phtml"))
|
|
28
|
+
hints.push("Use magento_workflow action=syntax-check when PHP syntax verification is relevant.");
|
|
29
|
+
if (value.endsWith(".xml"))
|
|
30
|
+
hints.push("Review the XSD result below; preview a cache clean only when this configuration change requires it.");
|
|
31
|
+
if (value.endsWith("etc/module.xml") || value.endsWith("db_schema.xml") || value.endsWith("composer.json"))
|
|
32
|
+
hints.push("This may require setup:upgrade; explain the evidence and use magento_workflow only after the user directly requests execution.");
|
|
33
|
+
return hints;
|
|
34
|
+
}
|
|
35
|
+
export function isMagentoXml(path) {
|
|
36
|
+
return path.toLowerCase().endsWith(".xml")
|
|
37
|
+
&& /(?:^|\/)(?:app\/(?:code|design)|vendor)\//.test(path.replace(/\\/g, "/"));
|
|
38
|
+
}
|
|
39
|
+
/** Validate changed Magento XML in its own selected environment using Magento's URN resolver. */
|
|
40
|
+
export async function validateMagentoXml(backend, path, signal) {
|
|
41
|
+
if (!isMagentoXml(path))
|
|
42
|
+
return "";
|
|
43
|
+
const output = await backend.run(xmlValidationCommand(path), { signal });
|
|
44
|
+
const detail = `${output.stdout}${output.stderr}`.trim();
|
|
45
|
+
return output.exitCode === 0
|
|
46
|
+
? detail || "Magento XML XSD validation passed."
|
|
47
|
+
: `Warning: Magento XML XSD validation failed for ${path}: ${detail || "validation could not run."}`;
|
|
48
|
+
}
|
|
49
|
+
export function xmlValidationCommand(path) {
|
|
50
|
+
return ["php", "-r", xsdValidator, path];
|
|
51
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,247 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: magento-
|
|
3
|
-
description: Magento Open Source
|
|
2
|
+
name: magento-extension-best-practices
|
|
3
|
+
description: Choose and review Magento Open Source / Adobe Commerce 2.4.x extension architecture. Load for non-trivial work involving DI, plugins, preferences, observers, service contracts, persistence, schema/XML, caching, indexers, queues, Admin UI, Luma, Hyvä, REST, GraphQL, security, or testing.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
+
# Magento Extension Best Practices
|
|
7
|
+
|
|
8
|
+
## Extension Decision Order
|
|
9
|
+
|
|
10
|
+
When changing existing behavior, prefer the first mechanism that fully solves the requirement:
|
|
11
|
+
|
|
12
|
+
1. XML or declarative configuration.
|
|
13
|
+
2. Public `@api` contract or documented extension point.
|
|
14
|
+
3. Existing strategy, pool, composite, provider, resolver, or other composition mechanism.
|
|
15
|
+
4. Semantic event + observer for an independent reaction.
|
|
16
|
+
5. `before` / `after` plugin for a narrow public-method change.
|
|
17
|
+
6. `around` plugin only when execution must be wrapped, skipped, or replaced.
|
|
18
|
+
7. Preference/inheritance for deliberate implementation replacement or non-interceptable behavior.
|
|
19
|
+
8. Undocumented internals only as an isolated, upgrade-sensitive last resort.
|
|
20
|
+
|
|
21
|
+
## Module Boundaries
|
|
22
|
+
|
|
23
|
+
- Prefer public `@api` contracts across modules.
|
|
24
|
+
- Isolate unavoidable concrete non-`@api` dependencies behind your own adapter/service and regression coverage.
|
|
25
|
+
- Put package dependencies in `composer.json`.
|
|
26
|
+
- Use `<sequence>` only when Magento config/setup/view load order matters.
|
|
27
|
+
- Create interfaces for public contracts, substitution boundaries, multiple implementations, or deliberately stable module boundaries; not for every service.
|
|
28
|
+
|
|
29
|
+
## DI and Object Creation
|
|
30
|
+
|
|
31
|
+
- **DI:** normal dependency known at wiring time.
|
|
32
|
+
- **Factory:** runtime/transient/entity instance creation.
|
|
33
|
+
- **Proxy:** lazy-load an expensive dependency that is often unused.
|
|
34
|
+
- **Virtual type:** same class with different constructor arguments in a specific injection context.
|
|
35
|
+
- **Preference:** deliberate implementation binding/replacement.
|
|
36
|
+
|
|
37
|
+
Do not use factories to hide ordinary dependencies or proxies as generic performance decoration.
|
|
38
|
+
|
|
39
|
+
## Plugins, Preferences, Events
|
|
40
|
+
|
|
41
|
+
### Plugins
|
|
42
|
+
|
|
43
|
+
- `before`: small argument changes.
|
|
44
|
+
- `after`: result changes.
|
|
45
|
+
- `around`: only when wrapping/skipping/replacing execution is required.
|
|
46
|
+
- If `before` or `after` works, do not use `around`.
|
|
47
|
+
- Keep plugins small and stateless.
|
|
48
|
+
- Delegate business logic to services.
|
|
49
|
+
- Do not plugin your own module when direct composition/refactoring is available.
|
|
50
|
+
- Interception is for supported public methods; final/non-public/static methods and constructors are not normal plugin targets.
|
|
51
|
+
|
|
52
|
+
### Preferences / Inheritance
|
|
53
|
+
|
|
54
|
+
Preferences are valid for deliberate implementation binding or full replacement.
|
|
55
|
+
|
|
56
|
+
Avoid a preference merely to change one public method. If replacement is necessary:
|
|
57
|
+
|
|
58
|
+
- override the minimum surface;
|
|
59
|
+
- do not copy whole core/vendor classes;
|
|
60
|
+
- isolate dependency on internals;
|
|
61
|
+
- add upgrade regression coverage.
|
|
62
|
+
|
|
63
|
+
### Events
|
|
64
|
+
|
|
65
|
+
Use observers for existing semantic events and independent side effects. Use plugins when changing a method's arguments, result, or execution.
|
|
66
|
+
|
|
67
|
+
Keep observers small and delegate work. Prefer area-specific `events.xml` when applicable. Do not rely on event-payload mutation as a hidden behavior override.
|
|
68
|
+
|
|
69
|
+
## Data Access
|
|
70
|
+
|
|
71
|
+
Repositories are module/API boundaries, not a universal internal persistence rule.
|
|
72
|
+
|
|
73
|
+
Use repositories/service contracts for public or cross-module entity access, standard CRUD/list contracts, `SearchCriteria`, API data interfaces, and extension attributes.
|
|
74
|
+
|
|
75
|
+
Internal code may use:
|
|
76
|
+
|
|
77
|
+
- collections for filtering/list/batch reads;
|
|
78
|
+
- resource models for persistence;
|
|
79
|
+
- dedicated query services for joins, reports, projections, or aggregates;
|
|
80
|
+
- `ResourceConnection` for deliberate bulk/index/import work.
|
|
81
|
+
|
|
82
|
+
Avoid new Active Record-style `$model->load()` / `$model->save()` usage.
|
|
83
|
+
|
|
84
|
+
### Direct SQL
|
|
85
|
+
|
|
86
|
+
Direct SQL is acceptable in resource/query layers when Magento entity lifecycle is intentionally unnecessary.
|
|
87
|
+
|
|
88
|
+
Use Magento DB adapters, resolved table names, and parameter bindings. Do not bypass required validation, business invariants, cache invalidation, events, or indexer/MView behavior.
|
|
89
|
+
|
|
90
|
+
### Performance and Concurrency
|
|
91
|
+
|
|
92
|
+
- Avoid repository/entity loads in large loops.
|
|
93
|
+
- Prevent N+1 extension-attribute and GraphQL loading.
|
|
94
|
+
- Prefer batch reads, joins, collections, or query services.
|
|
95
|
+
- Keep DB transactions small; avoid network calls inside long transactions.
|
|
96
|
+
- Use DB constraints, atomic updates, locks, or retries for concurrency-critical invariants.
|
|
97
|
+
- Make retryable queue/cron/bulk handlers idempotent where duplicate delivery or overlap is possible.
|
|
98
|
+
|
|
99
|
+
## Declarative Configuration
|
|
100
|
+
|
|
101
|
+
Before runtime PHP, check for an existing Magento mechanism:
|
|
102
|
+
|
|
103
|
+
`di.xml`, `events.xml`, `routes.xml`, `webapi.xml`, `acl.xml`, `system.xml`, `config.xml`, `menu.xml`, `email_templates.xml`, `cron.xml`, `communication.xml`, `queue_*`, `indexer.xml`, `mview.xml`, `extension_attributes.xml`, layout XML, UI component XML, `db_schema.xml`.
|
|
104
|
+
|
|
105
|
+
Respect global vs area-scoped configuration. Do not assume every XML type merges identically.
|
|
106
|
+
|
|
107
|
+
## Schema Changes
|
|
108
|
+
|
|
109
|
+
For new Magento 2.4.x modules:
|
|
110
|
+
|
|
111
|
+
- prefer `db_schema.xml` for schema;
|
|
112
|
+
- use data patches for one-time data changes;
|
|
113
|
+
- use schema patches only when imperative schema work is genuinely needed;
|
|
114
|
+
- treat `InstallSchema`, `UpgradeSchema`, `InstallData`, and `UpgradeData` as legacy patterns.
|
|
115
|
+
|
|
116
|
+
For large production tables, review locking, backfills, deployment safety, indexes, and foreign-key behavior.
|
|
117
|
+
|
|
118
|
+
## Cache and Indexers
|
|
119
|
+
|
|
120
|
+
Treat FPC as an architectural constraint.
|
|
121
|
+
|
|
122
|
+
- Avoid broad `cacheable="false"`.
|
|
123
|
+
- Keep customer-specific data out of public FPC output; use the proper private-content mechanism.
|
|
124
|
+
- Return correct cache identities/tags.
|
|
125
|
+
- Never global `cache:flush` after normal writes.
|
|
126
|
+
- Invalidate only relevant cache data or rely on Magento lifecycle invalidation.
|
|
127
|
+
- Include store/customer/website/authorization dimensions in cache variation when required.
|
|
128
|
+
|
|
129
|
+
Code must work with indexers in both **Update on Save** and **Update by Schedule** modes.
|
|
130
|
+
|
|
131
|
+
Do not full-reindex after every write. With direct SQL, verify index invalidation and MView changelog behavior.
|
|
132
|
+
|
|
133
|
+
## Async Work
|
|
134
|
+
|
|
135
|
+
Use message queues for slow, high-volume, retryable, or integration-heavy work.
|
|
136
|
+
|
|
137
|
+
Use cron mainly for scheduling/discovery; use consumers for scalable units of work. Prefer Magento queue configuration over bespoke polling infrastructure.
|
|
138
|
+
|
|
139
|
+
## Frontend
|
|
140
|
+
|
|
141
|
+
### Luma
|
|
142
|
+
|
|
143
|
+
Prefer:
|
|
144
|
+
|
|
145
|
+
1. Layout XML.
|
|
146
|
+
2. ViewModel.
|
|
147
|
+
3. Minimal template override.
|
|
148
|
+
4. RequireJS mixin for existing AMD behavior.
|
|
149
|
+
5. New JS module/component with declarative initialization.
|
|
150
|
+
6. Full replacement only when necessary.
|
|
151
|
+
|
|
152
|
+
Use ViewModels for template-facing behavior/data. Prefer `data-mage-init` / `x-magento-init`. Avoid copying whole vendor PHTML/JS files. Do not instantiate services in templates.
|
|
153
|
+
|
|
154
|
+
Use Knockout/UI Components where the existing Magento surface already uses them, especially checkout and complex Admin UI.
|
|
155
|
+
|
|
156
|
+
### Hyvä
|
|
157
|
+
|
|
158
|
+
Treat Hyvä as a separate frontend runtime.
|
|
159
|
+
|
|
160
|
+
- Do not assume RequireJS, Knockout, jQuery, or Luma `customer-data` on normal Hyvä pages.
|
|
161
|
+
- Prefer Alpine.js and Hyvä ViewModels/private-content mechanisms.
|
|
162
|
+
- Check installed Hyvä versions before relying on Tailwind/Alpine details.
|
|
163
|
+
- Use compatibility modules or isolated Luma fallback when appropriate.
|
|
164
|
+
|
|
165
|
+
## Admin
|
|
166
|
+
|
|
167
|
+
Use UI Components for standard Magento grids/forms where data sources, filters, bookmarks, mass actions, or existing component hierarchies are useful.
|
|
168
|
+
|
|
169
|
+
For simple bespoke pages, layout + block/ViewModel/template may be clearer.
|
|
170
|
+
|
|
171
|
+
Authorization must be backend-enforced. Controllers/routes and relevant UI data endpoints need proper ACL; hiding UI elements is not authorization.
|
|
172
|
+
|
|
173
|
+
## REST / SOAP / GraphQL
|
|
174
|
+
|
|
175
|
+
Expose stable service-contract methods through `webapi.xml` with correct ACL. Reuse business services across transports.
|
|
176
|
+
|
|
177
|
+
Keep GraphQL resolvers thin. Delegate logic, batch-load to avoid N+1, enforce authentication plus ownership/resource access, and return cache identities when required. Prefer token-based API auth over PHP-session-dependent API designs.
|
|
178
|
+
|
|
179
|
+
## State Boundaries
|
|
180
|
+
|
|
181
|
+
Do not inject checkout/customer/backend sessions, `RequestInterface`, cookies, or implicit request state into reusable business services.
|
|
182
|
+
|
|
183
|
+
Adapters should extract explicit context such as `customerId`, `quoteId`, `storeId`, or `websiteId` and pass it to services.
|
|
184
|
+
|
|
185
|
+
## Security
|
|
186
|
+
|
|
187
|
+
Verify:
|
|
188
|
+
|
|
189
|
+
- context-appropriate output escaping;
|
|
190
|
+
- CSRF/form-key and correct HTTP method for browser state changes;
|
|
191
|
+
- Admin/API/GraphQL ACL and object ownership;
|
|
192
|
+
- bound SQL parameters;
|
|
193
|
+
- safe filesystem/upload handling and validation;
|
|
194
|
+
- no unsafe unserialization or command execution on untrusted input.
|
|
195
|
+
|
|
196
|
+
## Testing
|
|
197
|
+
|
|
198
|
+
- **Unit:** pure services, value objects, algorithms.
|
|
199
|
+
- **Integration:** DI/XML, DB, repositories, plugins, observers, indexers, cache.
|
|
200
|
+
- **API functional:** REST/GraphQL contracts, auth, serialization.
|
|
201
|
+
- **MFTF:** critical browser workflows.
|
|
202
|
+
|
|
203
|
+
Test observable behavior, not interceptor implementation details. Add regression tests for upgrade-sensitive overrides.
|
|
204
|
+
|
|
205
|
+
## Red Flags
|
|
206
|
+
|
|
207
|
+
Redesign or explicitly justify:
|
|
208
|
+
|
|
209
|
+
- `ObjectManager::get()` in feature code;
|
|
210
|
+
- `$model->load()` / `$model->save()` in new code;
|
|
211
|
+
- large observers;
|
|
212
|
+
- business workflows in plugins;
|
|
213
|
+
- `around` for simple argument/result changes;
|
|
214
|
+
- preference for one public method;
|
|
215
|
+
- copied core/vendor classes or unnecessary copied PHTML/JS;
|
|
216
|
+
- repository/entity loads in large loops;
|
|
217
|
+
- global cache flush after normal writes;
|
|
218
|
+
- session/request state deep in business logic;
|
|
219
|
+
- direct SQL bypassing required Magento lifecycle;
|
|
220
|
+
- UI Components for every Admin page;
|
|
221
|
+
- direct edits under `vendor/`.
|
|
222
|
+
|
|
223
|
+
## Final Review
|
|
224
|
+
|
|
225
|
+
Before finalizing an implementation, verify:
|
|
226
|
+
|
|
227
|
+
1. Is there already an XML/declarative solution?
|
|
228
|
+
2. Is there a public API or narrower documented extension point?
|
|
229
|
+
3. Am I replacing more code than necessary?
|
|
230
|
+
4. Am I relying on non-public internals?
|
|
231
|
+
5. Could composition replace inheritance/preference?
|
|
232
|
+
6. Is `around` truly required?
|
|
233
|
+
7. Is business logic trapped in a plugin/observer/resolver/controller?
|
|
234
|
+
8. Is the data-access method appropriate, or merely habitual?
|
|
235
|
+
9. Can this create N+1 queries?
|
|
236
|
+
10. Does request/session state leak into reusable services?
|
|
237
|
+
11. Is it correct under FPC, indexers/MView, retries, concurrency, and production DI compilation?
|
|
238
|
+
12. Are ACL, ownership, escaping, SQL bindings, and CSRF correct?
|
|
239
|
+
13. Will it survive the next Magento/vendor patch update?
|
|
240
|
+
|
|
241
|
+
For production-sensitive changes, validate relevant paths with `setup:di:compile`, production mode, static content deployment where applicable, cache/indexer modes, cron/consumers, and Magento Coding Standard checks.
|
|
242
|
+
|
|
243
|
+
## MerchantDuo Magento 2.4 session workflow
|
|
244
|
+
|
|
6
245
|
# Magento 2.4
|
|
7
246
|
|
|
8
247
|
Confirm the installed Magento version, edition, and deployment mode before changing framework behavior. Prefer a focused module, declarative schema and data patches, dependency injection, service contracts, layout XML, and Magento CLI validation. Do not read `app/etc/env.php`.
|