chore: auto-enforce ARCHITECTURE.md upkeep + de-fragilize it #14

Keep the architecture map current without manual reminders, for the whole team:
- .claude/hooks/check-architecture-note.sh: Stop hook that blocks finishing a
  turn when src/ structure changes (file added/removed/renamed vs HEAD) without
  ARCHITECTURE.md being updated too. Untracked files & committed history are
  ignored (no false positives / no loops).
- .claude/settings.json: wires the Stop hook. Both versioned via a .gitignore
  un-ignore so Claude Code teammates inherit it (settings.local.json stays private).
- CLAUDE.md: pointer → hard RULE (update the map in the same commit).
- ARCHITECTURE.md: drop line counts (the only thing that drifted); note the rule.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
isUnknown 2026-06-21 14:34:11 +02:00
parent 227b5f9e66
commit 821bfbdf5e
5 changed files with 61 additions and 18 deletions

View file

@ -0,0 +1,27 @@
#!/usr/bin/env bash
# Stop hook — keep ARCHITECTURE.md in sync with the code structure.
#
# Nudges (blocks the turn from ending) when the current uncommitted change set
# adds / removes / renames a source file under src/ but does NOT also touch
# ARCHITECTURE.md. Looks at tracked changes vs HEAD only, so untracked scratch
# files don't trigger it and committed history is never re-flagged (no loops).
#
# Versioned in the repo so every Claude Code contributor inherits it.
cd "${CLAUDE_PROJECT_DIR:-.}" || exit 0
command -v git >/dev/null 2>&1 || exit 0
git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0
diff=$(git diff --name-status HEAD 2>/dev/null)
structural=$(printf '%s\n' "$diff" | grep -E '^(A|D|R)' | grep -E 'src/.*\.(vue|js)$')
note_changed=$(printf '%s\n' "$diff" | grep -E 'ARCHITECTURE\.md$')
if [ -n "$structural" ] && [ -z "$note_changed" ]; then
{
echo "src/ structure changed (file added/removed/renamed) without updating ARCHITECTURE.md:"
printf '%s\n' "$structural"
echo "ARCHITECTURE.md is the project's source-of-truth map — update it in the same change set before finishing."
} >&2
exit 2
fi
exit 0

14
.claude/settings.json Normal file
View file

@ -0,0 +1,14 @@
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/check-architecture-note.sh\""
}
]
}
]
}
}

6
.gitignore vendored
View file

@ -23,12 +23,12 @@ node_modules
*.sln
*.sw?
.claude
# Variables d'environnement Brevo
api/.env
# Claude settings
.claude
# Claude — ignore local/personal settings, but version the shared hook config for the team
/.claude/*
!/.claude/settings.json
!/.claude/hooks/
doc-conception.md

View file

@ -1,7 +1,8 @@
# ARCHITECTURE MAP (Claude-oriented, dense)
> Maintained by Claude for Claude. Goal: reconstruct the mental model of `/src` fast.
> Keep terse. Update when files move/merge or responsibilities change.
> Keep terse. **Rule: update this file in the SAME commit as any structural change** (file added/removed/renamed/moved, or a responsibility shift). A Stop hook (`.claude/hooks/check-architecture-note.sh`) reminds Claude Code sessions.
> No line counts here — they drift; describe structure & responsibility only.
> Legend: `→` calls/depends-on · `⊙` Pinia store · `✎` mutates customCss · `⚠` flag/gotcha · `☠` dead code (no live importer)
> Status: composables/stores/utils mapped. components/ = partial (only flags + tree); fill on demand.
@ -39,31 +40,31 @@ Central hub = **⊙stylesheet** (everything CSS funnels through it). Selection/v
## composables/ (real Vue composables only)
### Element settings (3-file unit, refactored — orchestrator/engine/model)
- `useElementSettings.js` (437) — ORCHESTRATOR for ElementPopup. Owns selector/currentTag refs, per-selector persistence (`elementStates` Map), toggle cache, open/close lifecycle (open→initTogglesAndCache/applyTagDefaults/applyStoredValues), all watchers. `guard={active,deactivateNextTick()}` suppresses watcher feedback during self-writes. Exposes flat API consumed by ElementPopup. → useElementStyleState, useElementCss, ⊙stylesheet, ⊙textDefaults, useProjectFonts, createDebouncer.
- `useElementCss.js` (450) ✎ — ENGINE: the only store↔refs↔preview boundary. writer(updateProp/removeProps/applyGroup/removeSpecialGroupProps), display computeds(displayedCss/editableFullCss/elementCss + hasInCss/isInline/isIndependent), sync(syncRefsFromCss→applyParsedGroupToRefs/enableGroupFromParsed/resetSpecialGroupToInherited, handleCssInput→writeCssBlock), loader(loadValuesFromStylesheet). helpers: parseLength/cssString.
- `useElementStyleState.js` (77) — reactive MODEL: one ref per CSS prop + settingEnabled + settingCache + styleProps/unitProps (from descriptors). No logic.
- `useElementSettings.js` — ORCHESTRATOR for ElementPopup. Owns selector/currentTag refs, per-selector persistence (`elementStates` Map), toggle cache, open/close lifecycle (open→initTogglesAndCache/applyTagDefaults/applyStoredValues), all watchers. `guard={active,deactivateNextTick()}` suppresses watcher feedback during self-writes. Exposes flat API consumed by ElementPopup. → useElementStyleState, useElementCss, ⊙stylesheet, ⊙textDefaults, useProjectFonts, createDebouncer.
- `useElementCss.js` ✎ — ENGINE: the only store↔refs↔preview boundary. writer(updateProp/removeProps/applyGroup/removeSpecialGroupProps), display computeds(displayedCss/editableFullCss/elementCss + hasInCss/isInline/isIndependent), sync(syncRefsFromCss→applyParsedGroupToRefs/enableGroupFromParsed/resetSpecialGroupToInherited, handleCssInput→writeCssBlock), loader(loadValuesFromStylesheet). helpers: parseLength/cssString.
- `useElementStyleState.js` — reactive MODEL: one ref per CSS prop + settingEnabled + settingCache + styleProps/unitProps (from descriptors). No logic.
### Preview / iframe / interaction
- `useIframeInteractions.js` (397) — hover labels + click routing inside iframe → element/image/carte popups. getSelectorFromElement, getContentElement, getImageFigure, getCarteFigure, handleIframeMouseMove/Click, popup-close handlers. (Page-template dead code removed → archive/page-template-styling/.)
- `usePreviewRenderer.js` (171) — double-buffered iframe render w/ crossfade + scroll-% persistence. Builds srcdoc. watches ⊙stylesheet.content & ⊙narrative.data → renderPreview. setKeyboardShortcutHandler. → Coloris.close on iframe click.
- `usePrintPreview.js` (70) — collect iframe styles, swap into document, window.print(), reload.
- `useKeyboardShortcuts.js` (78) — Cmd/Ctrl+S(save)/+P(print), Esc(close popup), `\`(toggle panel). Listens on document + attachToIframe.
- `useIframeInteractions.js` — hover labels + click routing inside iframe → element/image/carte popups. getSelectorFromElement, getContentElement, getImageFigure, getCarteFigure, handleIframeMouseMove/Click, popup-close handlers. (Page-template dead code removed → archive/page-template-styling/.)
- `usePreviewRenderer.js` — double-buffered iframe render w/ crossfade + scroll-% persistence. Builds srcdoc. watches ⊙stylesheet.content & ⊙narrative.data → renderPreview. setKeyboardShortcutHandler. → Coloris.close on iframe click.
- `usePrintPreview.js` — collect iframe styles, swap into document, window.print(), reload.
- `useKeyboardShortcuts.js` — Cmd/Ctrl+S(save)/+P(print), Esc(close popup), `\`(toggle panel). Listens on document + attachToIframe.
### Fonts / spacing
- `useProjectFonts.js` (167) — SINGLETON. PROJECT_FONTS list, @font-face gen, loadFont(name)→⊙stylesheet.setFontFaceCss, loadAllFontPreviews (select preview), loadFontsFromCss. loadedFontsCss computed.
- `useSpacingEditor.js` (79) — margin/padding reactive (4 sides) + lock + unit convert. Used by ElementPopup (margin/padding passed into useElementSettings).
- `usePageSettings.js` (185) — all @page state + CSS read/write for PageSettings.vue (format/dimensions, margins, background, page numbers/running title via updatePageFooters CSS surgery). PageSettings.vue is now a thin view (193, Coloris wiring only).
- `useProjectFonts.js` — SINGLETON. PROJECT_FONTS list, @font-face gen, loadFont(name)→⊙stylesheet.setFontFaceCss, loadAllFontPreviews (select preview), loadFontsFromCss. loadedFontsCss computed.
- `useSpacingEditor.js` — margin/padding reactive (4 sides) + lock + unit convert. Used by ElementPopup (margin/padding passed into useElementSettings).
- `usePageSettings.js` — all @page state + CSS read/write for PageSettings.vue (format/dimensions, margins, background, page numbers/running title via updatePageFooters CSS surgery). PageSettings.vue is now a thin view (Coloris wiring only).
## components/ (TODO: full map on demand)
Tree: App.vue → PagedJsWrapper(content-source), EditorPanel→{PageSettings,TextSettings,FigureDefaultsSettings×2(kind=image|carte),StylesheetViewer,ContentPanel→ContentTreeItem}, ElementPopup + FigureSettingsPopup×2 (kind=image|carte) (+BasePopup), PreviewLoader, SaveButton, PrintButton, ZoomControls. ui/: BasePopup, NumberInput, InputWithUnit, UnitToggle, ZoomControls, CssFileImport. blocks/: per content-block render (Text/Heading/Image/Video/Audio/Map/Quote/List) via blocks/index.js.
Popups expose imperatively (defineExpose: open/close/visible/handleClick); App wires refs into useIframeInteractions.
- `ui/SettingSection.vue` — reusable toggleable section (checkbox toggle + label header, body slot, optional #footer + #header-extra slots). Used by ElementPopup (10 sections) & FigureSettingsPopup (4). emits toggle/section-click. (Settings panels use raw non-toggle setting__section markup.)
- `ui/SettingSection.vue` — reusable toggleable section (checkbox toggle + label header, body slot, optional #footer + #header-extra slots). Used by ElementPopup & FigureSettingsPopup. emits toggle/section-click. (Settings panels use raw non-toggle setting__section markup.)
⚠ Popup close re-entry: BasePopup.close() emits 'close', wired back to the host's handleClose → its close logic. Host close MUST guard `if (basePopup.value?.visible) basePopup.value.close()` or it infinite-recurses.
- `FigureSettingsPopup.vue` (110, thin view) + `useFigureSettings.js` (composable) — per-figure width/height/margin/position editor for block-image & block-carte. prop `kind`('image'|'carte') selects baseClass + selector derivation + defaults store. Composable logic in named helpers: read{Width,Height,Margins,Position}FromCss (store→data), apply*, loadFromCss/restoreFromStored, parse*FromText/persistStateAsCss (manual edits). exposes handleClick(figure,event). (Merged from former ImagePopup/CartePopup.)
- `FigureSettingsPopup.vue` (thin view) + `useFigureSettings.js` (composable) — per-figure width/height/margin/position editor for block-image & block-carte. prop `kind`('image'|'carte') selects baseClass + selector derivation + defaults store. Composable logic in named helpers: read{Width,Height,Margins,Position}FromCss (store→data), apply*, loadFromCss/restoreFromStored, parse*FromText/persistStateAsCss (manual edits). exposes handleClick(figure,event). (Merged from former ImagePopup/CartePopup.)
- `editor/FigureDefaultsSettings.vue` — default width/height panel for images & cartes. prop `kind`; image targets [.block-image,.geoformat-cover-image], carte targets [.block-carte]; writes ⊙image/carteDefaults. (Merged from former ImageSettings/CarteSettings.)
## REFACTOR BACKLOG
- (low priority) `StylesheetViewer.vue`(369) & `ContentTreeItem.vue`(338) are long but mostly `<style>` CSS (~130-180 l.) with short, simple scripts — not worth splitting.
- (low priority) `StylesheetViewer.vue` & `ContentTreeItem.vue` are long but mostly `<style>` CSS with short, simple scripts — not worth splitting.
## ARCHIVED (removed from live tree, restorable)
- `archive/page-template-styling/` — "page template styling" feature (PagePopup + its wiring). See its REINSTALL.md. Removed orphans (no importer): useLinkedSpacing.js, ui/MarginEditor.vue, SidePanel.vue (git history only).

View file

@ -1,6 +1,7 @@
# GeoProject - Web-to-Print Interface
> 🤖 **Claude: read `ARCHITECTURE.md` (repo root) first** — dense file-by-file map of `/src` (stores/utils/composables fully mapped, components partial), data flow, and a dead-code/refactor backlog. Keep it updated as the code evolves.
> 🤖 **Claude: `ARCHITECTURE.md` (repo root) is the source-of-truth map of `/src`** — read it first (data flow, per-file responsibilities, dead-code/refactor backlog).
> **RULE:** any structural change (a file added / removed / renamed / moved, or a responsibility shift) MUST update `ARCHITECTURE.md` **in the same commit**. A Stop hook (`.claude/hooks/check-architecture-note.sh`, versioned in the repo) enforces this for Claude Code sessions.
## Vue d'ensemble