ModelType defines a formula with placeholders ({serie}{diametre}{type}).
ReferenceAutoGenerator resolves it from CustomFieldValues with trim+uppercase normalisation.
ReferenceAutoSubscriber (onFlush) recalculates on Piece/CFV insert/update/delete.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Backend:
- Enrich machine snapshot with componentLinks/pieceLinks/productLinks
- Detect link add/remove in MachineAuditSubscriber onFlush
- Add link diff comparison in restore preview
- Add link restoration in applyRestore for machines
- Add integrity warnings for missing linked entities
Frontend (submodule update):
- Single save button replacing auto-save-on-blur
- Link versioning display in version list and restore modal
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove DB unique index on composants.name and add Symfony UniqueEntity
validation on reference field with explicit error message.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All findBy(['machine' => ...]) queries now sort by createdAt ASC.
Without explicit ORDER BY, PostgreSQL returned rows in heap order which
changed on every INSERT, causing the displayed order to shuffle.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comments can now have documents attached via multipart/form-data upload.
New endpoint GET /api/documents/comment/{id} to list a comment's files.
Document entity gains a comment relation with cascade remove.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a ModelType's custom field was renamed without sending the field ID,
the service would create a new CustomField instead of reusing the existing
one, orphaning all CustomFieldValues. Now matches by orderIndex as fallback
before name, preserving the link to existing values.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tests cover:
- Clone: CustomFieldValue references cloned definitions, not source
- Clone: values are preserved after cloning
- Slots: 404 on non-existent piece, 422 on wrong type, success on correct type
- Conversion: blocked when slots have filled data, allowed when empty
- CustomField: rejects orphan creation, accepts existing field by ID
Also removes legacy JSON structure check (column no longer exists
after normalization) — replaced by slot table checks.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Frontend: always include constructeurs key in PATCH payload even when
empty, so merge-patch+json actually clears the relation
- Backend: add setConstructeurs() on Piece and Product entities (Composant
already had it) so API Platform can replace the ManyToMany collection
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The ComposantProcessor now reads the structure payload from the frontend
and applies selectedPieceId/selectedProductId/selectedComponentId to the
scaffolded slots, so user selections are actually saved to the database.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add ComposantProcessor: initializes piece/product/subcomponent slots
from ModelType skeleton requirements when a composant is created
- UniqueConstraintSubscriber: priority 256, French error messages,
constraint name detection for specific feedback
- Migration: scaffold missing slots for existing composants in prod
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Backend: match existing CustomField by name as fallback when ID is not provided,
preventing deletion and recreation of field definitions (which cascade-deletes values).
Includes restoration/migration scripts for prod:
- restore-custom-field-values.php: restores piece values from audit logs
- migrate-orphaned-custom-fields.php: migrates values from orphaned CFs
- fix-prod-all.php: combined fix (migrate + restore + cleanup)
- fix-prod-recreate-and-migrate.php: full fix (recreate missing CFs + migrate + restore)
- check-prod-*.php: diagnostic scripts
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tools now return CallToolResult directly instead of Content arrays,
preventing the MCP SDK from auto-generating structuredContent as a
JSON array (which Claude Code rejects — expects a JSON object/record).
Also adds Accept header to test helpers and SSE response parsing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- 3 MCP resources: schema, roles, stats
- docs/mcp/README.md with full user guide (config, tools catalogue, workflows)
- .mcp.json for Claude Code stdio transport
- Design spec and implementation plan
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- list_slots + update_slots for composant/piece slots
- list/add/update/remove machine links (component, piece, product)
- get_machine_structure with full hierarchy
- clone_machine with all links and custom fields
- 52 MCP tests pass total
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- 5 tools each: list, get, create, update, delete
- Piece: includes typePiece, constructeurs, prix (string)
- Composant: includes typeComposant, constructeurs, prix (string)
- Machine: includes site (required), constructeurs
- 40 MCP tests pass total
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- 5 tools each: list, get, create, update, delete
- McpToolHelper extracted to AbstractApiTestCase for reuse
- DashboardStatsToolTest simplified to use base helpers
- 22 MCP tests pass
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- McpStdioAuthSubscriber for console transport auth via env vars
- DashboardStatsTool as PoC (validates MCP protocol flow)
- McpToolHelper trait for shared pagination/error utilities
- Key learning: #[McpTool] must be on CLASS, not method for __invoke
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SkeletonStructureService was deleting all skeleton requirements and
recreating them on every ModelType update. Combined with position-based
matching in sync strategies, any reordering or insertion caused all
existing slots to be orphaned and recreated empty, losing selections.
- SkeletonStructureService: update requirements in-place by matching
on typeId instead of delete-all/recreate-all
- ComposantSyncStrategy & PieceSyncStrategy: two-pass smart matching
algorithm (exact typeId+position first, then typeId-only fallback)
to preserve selectedPiece/selectedComposant/selectedProduct on
reorder/insertion
- Frontend: check patch result.success before updating local state
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add selectedPieceId support to ComposantPieceSlotController
- Create ComposantProductSlotController and ComposantSubcomponentSlotController
- Add updateCustomFields() to SkeletonStructureService for managing CustomField entities
- Fix position/orderIndex fallback to array index in all 3 sync strategies
- Fix type comparison in ProductSyncStrategy for dual format support
- Update CLAUDE.md with new entities, controllers, and fixtures documentation
- Update frontend submodule with interactive slot selectors
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implement the sync orchestrator that delegates to tagged strategies via
AutowireIterator, and the HTTP controller exposing sync-preview and sync
endpoints with transaction wrapping and role-based access control.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Exposes structure as a computed property from pieceSlots, productSlots,
and subcomponentSlots relations, including slotId for frontend quantity
persistence.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
normalizeProduct() had customFields hardcoded to [] and was missing
customFieldValues entirely, unlike normalizeComposant and normalizePiece.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
getStructure() was returning hardcoded empty customFields arrays after
the JSON-to-tables migration. Now reads from the relational CustomField
entities via serializeCustomFields() for all three categories.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>