These rules enforce memory-aware behavior for AI coding agents.
If signet_memory_search is available, use Signet as the primary memory system.
Otherwise, use your native memory capabilities (MEMORY.md, auto memory, etc.).
-
Search memory before running commands. Before build/test/deploy/lint commands, search for the verified procedure. Use the stored version exactly. Skip for: single-line edits; commands the user gave you verbatim this turn. Preferred:
signet_memory_search(query, type, limit). Fallback: MEMORY.md or native recall. -
Search memory at session start. Look for recent session summaries before touching files. Before searching explicitly, check whether memory context is already available in your session. If it covers recent summaries and project-relevant notes, skip the explicit search. Search explicitly for: continuation requests (daily-log by project scope), project-specific recall the available context lacks, or when no memory context is available at all. Skip for: self-contained tasks; memory context already covers the current project.
-
Store conclusions BEFORE composing your answer. After multi-step investigations, decisions, or debugging, store the synthesized conclusion in memory FIRST — before writing the user-facing response. Sequence: investigate → synthesize → store → answer. If you are writing a response that contains a novel conclusion and have not yet stored it, stop, store it, then continue. Search for duplicates first — update, don't duplicate. When the conclusion is a user-stated hard constraint or critical procedure, set
pinned: truealongsideimportance: 1.0and tagcritical. Skip for: trivial Q&A under 3 exchanges; single lookups with no novel finding. Preferred:signet_memory_store(content, type, tags, importance, pinned). Fallback: native memory. -
Write a structured session handoff before ending non-trivial sessions. Store a daily-log with: accomplishments, decisions made, unfinished work, blockers — task-oriented synthesis for the next session to resume without re-reading the transcript. Skip for: sessions with no investigation/decision/exploration; sessions under 3 exchanges.
-
When memory returns no results, say so in one sentence and proceed.
Memory returned no results for "<query>". Checking project files.Memory gaps are normal. Do not retry with minor variations or distrust memory on subsequent searches. Then store the result so the gap fills over time. -
When memory conflicts with current code, trust the code. Code is the artifact; memory is commentary. When they disagree, the artifact wins. Update or remove stale memory. Exception: if the memory records a
decisionorrationaletype, flag the conflict to the user before updating — the code may have diverged intentionally. -
Use the correct memory type.
proceduralfor commands,decisionfor choices,preferencefor user habits. Do not default everything tofact.
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Matecat is an enterprise-level web-based Computer-Assisted Translation (CAT) tool. PHP 8.3+ backend with a React/Vite frontend. Uses Redis for caching, MySQL for persistence, and ActiveMQ for async job processing.
PSR-4 root is lib/ with empty namespace prefix. Classes in lib/Controller/API/App/FooController.php have namespace Controller\API\App. Plugin classes in plugins/*/lib/ follow the same pattern (e.g., Features\Translated).
lib/Controller/— HTTP controllers.Abstracts/contains the base chain:KleinController→BaseKleinViewController→ concrete controllerslib/Model/— Domain models, DAOs, structs. DAOs extendAbstractDaowithDaoCacheTraitfor Redis cachinglib/Utils/— Engines (MT/TM integrations), async workers, LQA, subfiltering, task runnerlib/Plugins/Features/— Internal features (ReviewExtended, TranslationVersions, SegmentFilter, ProjectCompletion)plugins/— External plugin submodules (translated, airbnb, uber, aligner, vite). Each haslib/Features/with a class extendingBaseFeaturelib/Model/FeaturesBase/— Event system.Hook/Event/Filter/for data-transforming events,Hook/Event/Run/for side-effect events.FeatureSetdispatches events to registered features
AbstractEngine → concrete engines (MyMemory, MMT, DeepL, Lara, Google, etc.) → Results/ response classes → EnginesFactory. Widest inheritance tree in the codebase.
Workers in lib/Utils/AsyncTasks/Workers/ process queued jobs via ActiveMQ. Key workers: TMAnalysisWorker, GetContributionWorker, SetContributionWorker, FastAnalysis, ProjectCreationWorker. Daemon entry points in daemons/.
AbstractDao → concrete DAOs. DaoCacheTrait provides Redis-backed caching with XFetch early recomputation. Structs extend AbstractDaoObjectStruct with ArrayAccessTrait. ShapelessConcreteStruct for untyped data.
DaoCacheTrait follows the transaction of the object using it, declared by _cacheTransactionScope().
AbstractDao returns its own injected IDatabase; Pager, UserStateStore and
SessionTokenStoreHandler return null, because they do not write through a transaction and a token revocation must
never wait behind a commit.
- A read taken inside an open transaction is not written to cache — it is not public yet, and a rollback would leave a row that never existed readable for the whole TTL.
- An eviction issued inside an open transaction is queued on
IDatabase::onCommit()and runs after the commit. Evicting before the commit is worse than not evicting: another connection misses the cache, reads the pre-commit row and caches it again, behind the eviction that just ran.
Callers do not schedule any of this. Do not wrap destroyCacheXxx() in onCommit() by hand.
onCommit($callback, critical: true) re-throws a failure once the rest of the queue has run — use it only where a
silent failure is a security problem, such as a credential sweep, not for a cache bust.
A transaction is opened only through IDatabase::transaction(callable). The outermost scope owns it; a scope entered
inside an open transaction is a guest that opens and closes nothing, so it cannot commit its caller's work early. Any
throw aborts the whole tree — including one the caller catches, because the failing scope marks the transaction unable
to commit and Database::commit()
refuses it. Work deferred with onCommit() drains once, after the single real commit, and is discarded on rollback.
begin(), commit() and rollback() are not on IDatabase, so code holding the interface — which is all of it —
cannot reach them. They stay public on Database for the test harness, which opens a fixture scope in setUp() and
rolls it back in tearDown(). Those three and
PDO::beginTransaction()/commit()/rollBack() are also reported by a PHPStan rule
(NoManualTransactionControlRule), which covers the receivers the interface cannot: a Database, a subclass of it, and
a bare PDO handle out of getConnection(). A raw commit leaves the deferral queue undrained and the next begin()
discards it. Do not wrap transaction control in a helper class or a trait either: two such facades already had to be
removed, and a type-based rule cannot see through them.
onCommit() is for code that does not own the scope. A DAO write cannot tell whether it is the outermost scope or
nested five calls deep, so it defers and lets the owner's commit drain the queue. When you do own the scope, put the
statement after transaction() returns instead — same effect, and you get the exception if it fails, where a queued
callback only logs it.
A cache key is md5(query . bind params), so the query is half the address: two reads can bind the
same values and differ only in their SQL. Name an eviction after the read it deletes — that
read's name, find/get prefix dropped — never after the parameters.
| tier | name | takes | clears | visibility |
|---|---|---|---|---|
| door | destroyCache |
the struct | every address | public, one per DAO |
| fan-out | destroyCaches<Address> |
key components | one address, N reads | public only with a named caller |
| leaf | destroyCache<ReadName> |
bind values | one read | private by default |
The door is the only tier taking a struct, which is what separates it from a leaf at a call site.
Leaves return bool, a door void. Never add a For, never keep the read's find. A DAO binding
a caller-supplied key (both MetadataDao) cannot have a door.
The character set of the database, its tables and the connection is infrastructure. Never set, change or work around it from PHP.
MateCat is open source and every installation owns its own schema. INSTALL/matecat.sql ships
utf8mb4, which is what a fresh self-hosted install gets. Older installations are utf8mb3, and
tests/inc/unittest_matecat_local.sql matches those. Several tables — project_templates,
filters_config_templates, xliff_config_templates, payable_rate_templates — are created by migrations with no
charset clause at all and inherit the database default; the two mt_qe_* name
columns are explicitly CHARACTER SET latin1. All of these are legitimate. None is drift to be reconciled.
PHP knows nothing about any of it, and that is correct — it is the property that lets one codebase run on every installation. Do not treat it as a gap to close: never read, assume or encode a storage charset in application code. A rule written for three-byte storage is needlessly strict where four bytes are available; one written for four truncates silently where they are not. Neither belongs in the code.
Database::getConnection() opens every connection with SET NAMES utf8 (utf8mb3), and does it twice — once as
MYSQL_ATTR_INIT_COMMAND and once as a bare exec(). Model\Conversion\Filters
does the same on its own connection. Those lines are not a bug to tidy. Changing the connection charset alone, against
columns that are narrower, corrupts or truncates across every query with no error and nothing shown to the user.
Widening a column is an ALTER TABLE per column, plus the connection, plus the index key widths that follow from four
bytes per character — coordinated, in a migration window, owned by whoever runs that installation. Never a code edit,
never a rider on a feature PR.
Application code adapts to what the storage can hold rather than changing it.
UserSuppliedName::assertNoAstral() is the pattern: refuse on the narrower assumption where the user can see the 400,
strip where a throw would break the request (the OAuth callback, project creation), and explain the limit without naming
a charset.
# Full test suite (excludes tests needing external services)
vendor/bin/phpunit --exclude-group=ExternalServices --no-coverage
# Single test file
vendor/bin/phpunit tests/unit/Path/To/TestFile.php --no-coverage
# Single test method
vendor/bin/phpunit --filter testMethodName --no-coverage
# With coverage (requires XDEBUG_MODE=coverage)
XDEBUG_MODE=coverage vendor/bin/phpunit tests/unit/Path/To/TestFile.php --coverage-clover /tmp/coverage.xml- Tests mirror source structure:
lib/Utils/Foo/Bar.php→tests/unit/Utils/Foo/BarTest.php - Plugin tests:
plugins/*/tests/ - Predis
Clientuses__callmagic for Redis commands — cannot be mocked with PHPUnitcreateMock(). ExtendClientor mockRedisHandlerinstead
# Full codebase
vendor/bin/phpstan analyse --configuration=phpstan.neon --no-progress --error-format=table
# Single file (must report 0 errors for clean files)
vendor/bin/phpstan analyse path/to/File.php --configuration=phpstan.neon --no-progress --error-format=table- Level 8, no baseline — every reported error must be fixed (there is no
phpstan-baseline.neon) checkTooWideThrowTypesInProtectedAndPublicMethods: true— must use precise exception types in@throwsmissingCheckedExceptionInThrows: true— all thrown exceptions must be declaredUnknownPropertyExceptionis unchecked (used by structArrayAccessTrait)- When adding exceptions to PHPDoc, prefer
useimports over FQCN
yarn watch # Dev server with HMR
yarn build:dev # Development build
yarn build:production # Production buildEvery English string the app shows a user is sentence case — a capital on the first word only. This covers UI labels, buttons, headings and page titles, API/AJAX error payloads, exception messages, and email subjects and bodies.
Keep existing capitals for:
- Proper nouns — Matecat, Lara, DeepL, MyMemory, ModernMT, Google, Amazon S3, Intento,
Apertium, AltLang, SmartMATE, XTRF. In prose the brand is Matecat;
MateCatbelongs only to identifiers such asMateCatFilter. - Acronyms — API, ID, UID, URL, TM, MT, QA, QE, CSV, TMX, XML, JSON, XLIFF, DB, ZIP, MIME, JWT, SQL, HTTP, IP.
- Code identifiers quoted in a message — class, method and parameter names
(
TeamStruct,getInstance(),id_job,batchSize). A message that opens with one keeps its lowercase:'id_job not valid', not'Id_job not valid'.
Volume analysis not Volume Analysis
Invalid upload token. not Invalid Upload Token.
Not authorized not Not Authorized
Wrong ID project provided not Wrong Id project provided
ZIP error: not Zip error:
is mandatory not is MANDATORY
Exception messages are user-facing. router.php maps every exception class to an HTTP
status and serializes it through View\API\Commons\Error, which copies getMessage()
straight into errors[0].message of the JSON response — with no PRINT_ERRORS guard, and
including the fallback 500 branch.
Do not re-case — these are contracts or protocol, not copy:
- Data exports: the QA-report CSV headers in
lib/View/API/V2/Json/SegmentTranslationIssue.phpand the plain-text analysis report inlib/Model/Analysis/XTRFStatus.php. - API response keys: the
$SUPPORTED_FILE_TYPESgroup names inlib/Utils/Registry/AppConfig.phpare emitted verbatim bylib/Controller/API/V2/SupportedFilesController.php. - HTTP reason phrases (
header('HTTP/1.1 400 Bad Request')) and User-Agent strings. - Strings compared against a third-party API's own responses (see
lib/Utils/TMS/TMSService.php,lib/Controller/API/GDrive/GDriveController.php). - Internal
logger->debug()/error()output and timing array keys.
When you add or change one of these strings:
- Grep
tests/for the old text before you finish. Many tests pin exact messages, andexpectExceptionMessage()/assertStringContainsString()match substrings — a full-literal grep misses an assertion on a fragment like'Zip error'. DB-backed tests (*RealSqlTest.phpand others) only execute in CI, because host PHP has nopdo_mysql; a green local run does not mean they pass. - Edit
lib/View/templates/_*.html, neverlib/View/*.html— the latter are git-ignored build artifacts regenerated from the templates by the VitehtmlTemplatePlugin. - Check for a CSS override.
text-transform: capitalizerenders Title Case whatever the string says; that rule used to sit onh1and.btn ainlib/View/Emails/skeleton.html. - Grep beyond
throw new. Exceptions passed as an argument (someCall($x, new DomainException("…"))) span lines, and single mid-sentence capitals (and Teams,the Assignee) have no adjacent-capital pair to match on.
Do not add Co-Authored-By trailers to commit messages.
Do not add any reference to AI or AI tooling anywhere — commit messages, PR titles/bodies, code,
comments, or docs. This includes footers/signatures (🤖 Generated with Claude Code, Co-Authored-By
AI trailers), "generated/assisted by" lines, and tool names.
ONLY place references to AI it in the section designated for that purpose in the PR template's AI Disclosure section.
Follow the .github/PULL_REQUEST_TEMPLATE.md AND the .github/scripts/pr-readiness-check.js when creating a Pull
Request.
Follow the project .github/prompts/conventional-commit.prompt.md for commit message formatting:
- Format:
<emoji> <type>(<scope>): <description>(see emoji table below) - Show commit message first, wait for user approval before committing
- Use
git commit -a(lowercase), never-A - 100 character line limit
- Imperative mood, no capitalization, no period
Valid emoji Type Reference
| Type | Title | Emoji | Description | Example Scopes (non-exaustive) |
|---|---|---|---|---|
| build | Builds | 🏗️ | Changes that affect the build system or external dependencies | gulp, broccoli, npm |
| chore | Chores | 🔧 | Other changes that don't modify src or test files | scripts, config |
| ci | Continuous Integrations | 👷 | Changes to our CI configuration files and scripts | Travis, Circle, BrowserStack, SauceLabs,github actions, husky |
| docs | Documentation | 📝 | Documentation only changes | README, API |
| feat | Features | ✨ | A new feature | user, payment, gallery |
| fix | Bug Fixes | 🐛 | A bug fix | auth, data |
| security | Security Fixes | 🔒 | A change that fixes a vulnerability or hardens against one | auth, idor, xss, injection |
| perf | Performance Improvements | ⚡️ | A code change that improves performance | query, cache |
| refactor | Code Refactoring | ♻️ | A code change that neither fixes a bug nor adds a feature | utils, helpers |
| revert | Reverts | ⏪️ | Reverts a previous commit | query, utils, |
| style | Styles | 💄 | Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) | formatting |
| test | Tests | ✅ | Adding missing tests or correcting existing tests | unit, e2e |
| i18n | 🌐 | Internationalization | locale, translation | |
| merge | Merges | 🔀 | Merges a branch into another; the emoji is optional here, since git and the forge write their own | develop, master |
When creating worktrees, those commands MUST be used:
cd <project-root> && git branch --show-currentgit branch <branch-name> <current-branch-name>git worktree add ../matecat-<branch-name> <branch-name>cp composer.phar ../matecat-<branch-name>/composer.pharcd ../matecat-<branch-name>/ && php composer.phar installgit submodule update --init --recursive
When testing or calling HTTP/API endpoints, use the bruno-mcp MCP server first.
Workflow: list_collections → list_requests → run_collection.
Do not use curl or direct HTTP calls when Bruno collections exist.
Use dev environment for testing.
IMPORTANT: This project has a knowledge graph. ALWAYS use the code-review-graph MCP tools BEFORE using Grep/Glob/Read to explore the codebase. The graph is faster, cheaper (fewer tokens), and gives you structural context (callers, dependents, test coverage) that file scanning cannot.
- Exploring code:
semantic_search_nodes_toolorquery_graph_toolinstead of Grep - Understanding impact:
get_impact_radius_toolinstead of manually tracing imports - Code review:
detect_changes_tool+get_review_context_toolinstead of reading entire files - Finding relationships:
query_graph_toolwith callers_of/callees_of/imports_of/tests_for - Architecture questions:
get_architecture_overview_tool+list_communities_tool
Fall back to Grep/Glob/Read only when the graph doesn't cover what you need.
| Tool | Use when |
|---|---|
detect_changes_tool |
Reviewing code changes — gives risk-scored analysis |
get_review_context_tool |
Need source snippets for review — token-efficient |
get_impact_radius_tool |
Understanding blast radius of a change |
get_affected_flows_tool |
Finding which execution paths are impacted |
query_graph_tool |
Tracing callers, callees, imports, tests, dependencies |
semantic_search_nodes_tool |
Finding functions/classes by name or keyword |
get_architecture_overview_tool |
Understanding high-level codebase structure |
refactor_tool |
Planning renames, finding dead code |
- The graph auto-updates on file changes (via hooks).
- Use
detect_changes_toolfor code review. - Use
get_affected_flows_toolto understand impact. - Use
query_graph_toolpattern="tests_for" to check coverage.