Commit Graph

87 Commits

Author SHA1 Message Date
4df930bfe6 fix: persister le token même dans le chemin d'erreur de get_informations/get_messages
Cause racine : quand pronotepy reçoit une PronoteAPIError (ex. code 20 « page
expirée »), il appelle refresh() en interne, qui peut roter le token en mémoire
(client.password mis à jour avec un nouveau jetonConnexionAppliMobile). Le retry
peut aussi échouer — l'exception atteint get_informations()/get_messages() qui
l'attrapent et retournent [] (mode dégradé). Mais _persist_credentials()
n'était appelé que dans le chemin de SUCCÈS — le token rafraîchi en mémoire
n'était jamais persisté. Au run suivant, token_login utilisait le token
périmé → échec KeyError 'dataSec' → PronoteAuthRotationError.

Correction : appeler _persist_credentials() aussi dans le chemin d'erreur de
get_informations() et get_messages(), avant le return []. L'ancien token est
déjà invalidé côté serveur lors du refresh — ne pas persister le nouveau token
garantit la perte du seul token valide.

Tests : 3 nouveaux tests (persistance dans le chemin d'erreur, sauvegarde du
token rafraîchi sur erreur). 694 passés, couverture 94.93%.
2026-09-10 13:34:51 +02:00
d26cef8d3d fix: persister le token après chaque opération de données + corriger doc QR code
Cause racine : pronotepy peut rafraîchir (rotater) le token en mémoire pendant
l'exécution via refresh() automatique après une PronoteAPIError. L'ancien code
ne persistait les credentials qu'après le login initial, pas après les
opérations de données. Le token roté en mémoire était perdu → au run suivant,
token_login échouait avec le token périmé (KeyError 'dataSec').

Correction :
- PronoteClient._persist_credentials() : méthode centralisée qui persiste
  export_credentials() après chaque opération réussie (get_lessons,
  get_homeworks, get_messages, get_informations)
- Le token rafraîchi par le serveur pendant l'exécution est maintenant
  toujours persisté, même si le pipeline échoue ensuite

Documentation :
- .env.example : variables QR plus visibles (exemple qr_token décommentable)
- AGENTS.md : QR code depuis le site web Pronote (pas l'app mobile),
  persistance après chaque opération de données
- Wiki GuidePronote : procédure corrigée (site web, pas app Android/iOS),
  mention de la persistance après chaque opération

Tests : 5 nouveaux tests de persistance (691 passés, couverture 94.92%)
2026-09-10 12:10:42 +02:00
7dc48f6f43 docs: nettoyer AGENTS.md des règles redondantes
Retrait des sections de règles de développement de AGENTS.md, désormais centralisées dans orchestrator.md.

Co-Authored-By: Warp <agent@warp.dev>
2026-09-10 11:30:33 +02:00
bc79ebf680 docs: création d'un document de référence (non officiel) pour l'authentification pronote. 2026-09-10 11:25:49 +02:00
0363898669 feat: authentification QR code / token pour Pronote
Ajoute le mode d'authentification PRONOTE_AUTH_MODE=qr_token comme alternative
au mode password pour les instances Pronote utilisant HubEduConnect/EduConnect
où l'authentification par mot de passe échoue (CAPTCHA, MFA, flux SAML).

Nouveaux éléments :
- PronoteSettings : auth_mode, qr_code_file, qr_pin (SecretStr)
- PronoteAuthState : persistance du token rotatif dans .pronote_auth_state.json
  (écriture atomique, permissions 0600, symlink-safe via O_EXCL|O_NOFOLLOW)
- PronoteClient._connect_qr_token() : token_login avec creds persistés,
  qrcode_login pour l'enrôlement initial, export_credentials persisté après
  chaque login réussi
- PronoteAuthRotationError : levée en cas d'échec de rotation du token,
  propagée sans wrapping à travers PronoteFetcher et fetch_step jusqu'à
  PipelineRunner.run() qui notifie via XMPP (si canal disponible et dry_run inactif)
- _is_pronotepy_configured() mode-aware : qr_token ne requiert que PRONOTE_URL
- _collect_auth_secrets() : redaction des secrets explicites (token, PIN, jeton QR)
  dans tous les logs du chemin d'authentification

Documentation :
- .env.example : PRONOTE_AUTH_MODE, PRONOTE_QR_CODE_FILE, PRONOTE_QR_PIN
- AGENTS.md : contrat d'authentification QR code / token
- Wiki GuidePronote : section enrôlement, exécutions suivantes, ré-enrôlement

Tests (686 passés, couverture 94.87%) :
- 5 tests config QR, 9 tests auth_state, 10 tests client QR, 3 tests propagation,
  4 tests intégration rotation end-to-end, 4 tests fallback mode-aware
- Tests de non-fuite : sentinelles distinctes pour token, PIN, jeton QR

Co-authored-by: coder/litellm/coder <coder@agents.invalid>
2026-09-08 23:15:06 +02:00
4a6207f716 docs: corriger .env.example et enrichir GuidePronote (ENT obligatoire)
Co-authored-by: Antoine Van Elstraete <antoine@van-elstraete.net>
Co-committed-by: Antoine Van Elstraete <antoine@van-elstraete.net>
2026-09-08 21:40:48 +02:00
3b38253575 fix: PRONOTE_URL ignoré à cause du double préfixe env_prefix
Le champ pronote_url dans PronoteSettings avec env_prefix=PRONOTE_ produisait PRONOTE_PRONOTE_URL au lieu de PRONOTE_URL. Renomme le champ en url pour que le mécanisme standard produise PRONOTE_URL. Toutes les références mises à jour dans le code de production et les tests. Décision d'architecture : renommage préféré à un contournement par alias (mypy + dette technique).

Co-authored-by: Antoine Van Elstraete <antoine@van-elstraete.net>
Co-committed-by: Antoine Van Elstraete <antoine@van-elstraete.net>
2026-09-08 21:24:10 +02:00
bf4038814a docs: politique de versionnage et releases dans AGENTS.md
Co-authored-by: Antoine Van Elstraete <antoine@van-elstraete.net>
Co-committed-by: Antoine Van Elstraete <antoine@van-elstraete.net>
2026-09-08 21:04:22 +02:00
82b9877aad fix: PRONOTE_ENT optionnel pour les connexions pronotepy directes
PRONOTE_ENT était incorrectement traité comme obligatoire pour pronotepy. Rend ENT optionnel pour les connexions directes, conformément à la spécification. Corrige le mode pronotepy explicite et le mode auto sans iCal. 10 tests de régression ajoutés.

Co-authored-by: Antoine Van Elstraete <antoine@van-elstraete.net>
Co-committed-by: Antoine Van Elstraete <antoine@van-elstraete.net>
2026-09-08 20:54:03 +02:00
c851f67172 docs: fichier de vacances scolaires Bordeaux (zone A, 2026-2027)
Crée data/school_holidays.json avec les dates officielles de l'académie de Bordeaux (zone A) pour 2026-2027. Corrige l'erreur au démarrage quand SCHOOL_HOLIDAYS_PATH pointe vers un fichier inexistant. Documentation wiki : page GuideAgendaVacancesScolaires créée et liée dans le sidebar.

Co-authored-by: Antoine Van Elstraete <antoine@van-elstraete.net>
Co-committed-by: Antoine Van Elstraete <antoine@van-elstraete.net>
v0.1.1
2026-09-08 20:30:47 +02:00
4ac5be4c8d fix: corrige l'attribution de pronote-digest vers Yoan Bernabeu
L'attribution pointait à tort vers Antoine Coulon et le dépôt
antoine-coulon/pronote-digest. Le projet pronote-digest a été créé par
Yoan Bernabeu (https://yoanbernabeu.github.io/pronote-digest/, dépôt
https://github.com/yoanbernabeu/pronote-digest).

Co-authored-by: opencode/orchestrator <opencode-orchestrator@agents.invalid>
2026-09-08 18:57:55 +02:00
fdd3310462 fix: corrige l'attribution de pronote-digest vers Yoan Bernabeu
L'attribution pointait à tort vers Antoine Coulon et le dépôt
antoine-coulon/pronote-digest. Le projet pronote-digest a été créé par
Yoan Bernabeu (https://yoanbernabeu.github.io/pronote-digest/, dépôt
https://github.com/yoanbernabeu/pronote-digest).

Co-authored-by: opencode/orchestrator <opencode-orchestrator@agents.invalid>
2026-09-08 18:56:44 +02:00
0be02a660a docs: add M15 (Documentation) to CHANGELOG 0.1.0 entry v0.1.0 2026-09-08 18:33:40 +02:00
d85733116a chore: ignore HANDOFF.md and clean up temporary work files
Add HANDOFF.md to .gitignore for session handoff notes.
Delete 12 temporary files (FIXME_M1-M10, FEAT_M9, TEST_REPORT).
2026-09-08 17:55:55 +02:00
2deeb83c76 feat(M15): README, README.LLM.md, LICENSE, CHANGELOG, and final cleanup
Complete the M15 documentation and final review milestone:

README.md (French, brief):
- Project description, quick start, usage, deployment link, acknowledgments
- Acknowledgments: pronotepy, icalendar, caldav, slixmpp, pydantic, openai,
  feedparser, beautifulsoup4, and inspiration from pronote-digest (Antoine Coulon)
- Author: Antoine Van Elstraete

README.LLM.md (English, AI agent guide):
- Prerequisites, installation on LXC/VPS (Debian/CentOS)
- Non-secret configuration preparation (all env vars listed)
- Secret variables clearly identified as operator-only
- Pre-deployment checks (check_secrets.py, pip check, dry-run)
- systemd/timer/logrotate installation steps
- Notes for AI agent (do not commit secrets, do not modify existing files)

LICENSE:
- Standard MIT License, copyright Antoine Van Elstraete (2026)

CHANGELOG.md:
- Initial changelog following Keep a Changelog format
- Covers M1 through M14 with milestone summaries
- Gitea Actions noted as planned/optional, not delivered

.gitignore:
- Added FEAT_* pattern for temporary work files

TODO.md:
- M15 items 1-4 checked (README, architecture docs, CHANGELOG+LICENSE, final review)
- M15 item 5 (Gitea Actions) left unchecked (optional, not done)

Co-authored-by: opencode/tech-writer <tech-writer@agents.invalid>
m15-documentation
2026-09-08 17:52:45 +02:00
b518508632 chore: replace GitHub Actions CI/CD with Gitea Actions (LXC/VPS)
Replace GitHub Actions references with Gitea Actions for deployment
on LXC/VPS (Debian/CentOS):

TODO.md:
- M15 optional item: GitHub Actions CI/CD -> Gitea Actions (LXC/VPS)
- Acceptance criterion: "La CI exécute..." -> "Gitea Actions exécute..."

GUIDE_DEV_PYTHON.md:
- §Prochaines étapes: "Configurer CI/CD" with GitHub Actions -> Gitea
  Actions for LXC/VPS (Debian/CentOS)

Other github.com references (library URLs, pre-commit repos, project
URLs) remain unchanged — they are legitimate technical references.
2026-09-08 17:33:17 +02:00
b474f02e90 fix(M14): harden secret scanner — prefixed vars, index blobs, extensionless files
Correct five findings from independent review and security audit of the
M14 deployment secret scanner:

scripts/check_secrets.py:
- Regex: \b[a-z0-9_]* prefix before sensitive keywords to detect
  PRONOTE_PASSWORD, CALDAV_PASSWORD, AI_API_KEY and similar prefixed
  variable names (was: \b which doesn't match before underscore)
- Regex: minimum secret value length reduced from {8,} to {3,} for
  literal, unquoted, and URL parameter patterns
- Regex: unquoted pattern {3,} -> {2,} for 3-char total minimum
- --staged: reads Git index blobs via `git show :<path>` instead of
  working-tree files (ContentProvider type alias, _staged_content_provider)
- Extensionless deployment files: _EXTRA_NAMES allowlist for pronote_sync
- ContentProvider type alias documented with #: Sphinx comment

tests/unit/test_check_secrets.py (4 new tests, 9 total):
- test_main_detects_prefixed_secret_assignment: PRONOTE_PASSWORD detected
- test_main_detects_short_secret_assignment: 6-char secret detected
- test_staged_mode_reads_index_content_not_working_tree: working-tree
  content set to non-matching value to distinguish index from worktree
- test_main_scans_extensionless_deployment_file: pronote_sync scanned

TODO.md: all 5 M14 checklist items checked

Validation: 636 tests, coverage 95.67%, ruff/mypy/bandit/pre-commit green.

Co-authored-by: opencode/coder <coder@agents.invalid>
m14-deployment
2026-09-08 17:20:08 +02:00
e6e4b10047 Merge branch 'main' into feature/m14-deployment 2026-09-08 16:43:15 +02:00
d60357a017 feat(M14): add deployment artifacts and secret check
Co-authored-by: Codex/gpt-5.6-terra <codex-gpt-5.6-terra@agents.invalid>
2026-09-08 16:42:59 +02:00
000416f24e feat(M13): complete test fixtures, shared conftest, and coverage >= 90%
Finalize M13 test and coverage milestone:

tests/fixtures/pronote-6e.ics (new):
- Anonymized iCal fixture for Classe de 6e (3 VEVENTs: SVT lesson with
  homework block, Histoire-Géo modified lesson, all-day school outing)
- Same structure as pronote-4e.ics, no secrets or real data

tests/conftest.py:
- Added sample_message fixture (Message with MessageType.INFORMATION)
- Integrated sample_message into pronote_data fixture (messages=[sample_message])
- Sphinx/reST docstring with :return: and :rtype:

.gitignore:
- Fixed typo: .worktress/ -> .worktrees/ (line 56)

pyproject.toml:
- Added ".worktrees" to ruff extend-exclude to prevent ruff format --check .
  from scanning worktree files

TODO.md:
- Checked M13 items: tests/fixtures/ and tests/conftest.py

Validation: 627 tests pass, coverage 95.67% (threshold 90%), ruff/mypy/
bandit/pre-commit all green.

Co-authored-by: opencode/coder <coder@agents.invalid>
m13-tests-coverage
2026-09-08 16:19:27 +02:00
fd9b604849 feat(M12): CLI entry point with dry-run, log-level, redacted error display
Implement the CLI entry point for pronote-sync:

cli/main.py:
- main() entry point with --dry-run (tri-state: None defers to settings,
  True overrides) and --log-level (choices: DEBUG/INFO/WARNING/ERROR/CRITICAL)
- setup_logging called before settings load (to capture config errors),
  then reconfigured with settings.app.log_level
- PipelineRunner.from_settings() as composition root, runner.run()
- Return codes: 0 success, 1 failure, 2 argparse rejection
- _safe_traceback: strips exception messages, replaces with "erreur expurgée",
  walks __cause__/__context__ with cycle protection
- _settings_secrets: collects redaction_secrets() + usernames + JID/recipient
- All error messages redacted via redact_secrets() with configured secrets
- DEBUG-level traceback only shown when DEBUG is enabled

cli/__init__.py:
- Module docstring added (French, Sphinx/reST)

tests/e2e/test_cli.py (8 tests):
- Dry-run and log-level propagation to composition root
- Configured dry-run preserved (tri-state None)
- Success with warnings returns 0
- Pipeline error redaction at DEBUG (sentinel secret)
- Configuration failure redacted traceback at DEBUG
- Pronote username non-disclosure
- Unexpected pipeline exception: redacted traceback at DEBUG, no traceback at INFO
- Argparse rejection of unknown log level (exit code 2)

Coverage: cli/ 94.74%, 627 total tests pass.

Co-authored-by: opencode/coder <coder@agents.invalid>
Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
m12-cli-entrypoint
2026-09-08 15:57:28 +02:00
1019b22808 docs(M11): align GUIDE sections 4.2.1 and 11.3 with FIXME_M11 corrections
GUIDE_DEV_PYTHON.md:
- §11.3: add except PipelineCriticalError: raise before each non-critical
  except in the illustrative PipelineRunner.run() code
- §11.3: replace redact_exception(exc) with self._redact(exc) in all except
  blocks, add explanatory paragraph about _redaction_secrets and _redact()
- §11.3: fix Google-style Returns: to Sphinx/reST :return: and :rtype:
- §11.3: fix malformed Markdown code fence (get_errors/get_warnings orphaned)
- §4.2.1: fix redact_exception() example to pass extra_secrets to
  redact_secrets() in the return statement

TODO.md M11:
- Add and check criterion: PipelineCriticalError from non-blocking step
  stops the pipeline

.secrets.baseline:
- Line numbers updated for documentation shifts

Co-authored-by: opencode/tech-writer <tech-writer@agents.invalid>
m11-pipeline
2026-09-08 12:47:42 +02:00
28c695795a fix(M11): propagate PipelineCriticalError, redact configured secrets, signal blog failures
Correct 4 findings from the independent M11 review:

#1 (Critical) — PipelineCriticalError was downgraded to PipelineWarning:
  - Add except PipelineCriticalError: raise before each except Exception
    in all 5 non-blocking steps (fetch_blog, compare, caldav_sync, synthesis, send)
  - Critical errors now propagate to the outer handler and stop the pipeline

#2 (Critical) — redact_exception() did not use configured secrets:
  - Extend redact_exception() with extra_secrets parameter (upward compatible)
  - Harden redact_secrets(): sort extra_secrets by length descending
  - Add Settings.redaction_secrets() collecting all 6 SecretStr fields
  - Add PipelineRunner._redact(exc) using self._redaction_secrets
  - All except blocks in run() now use self._redact(exc)
  - CalDAV FAILED-status path uses full redaction_secrets collection

#3 (Medium) — BlogRSSClient silently swallowed failures:
  - Add error field to BlogRSSFetchResult
  - rss.py sets error on failure paths (except Exception, bozo/invalid feed)
  - fetch_blog_step raises RuntimeError when result.error is set
  - PipelineRunner now produces PipelineWarning for blog failures

#4 (Medium) — Test coverage at 80%, now 91%:
  - 11 new integration tests covering blog failure/success, compare failure,
    CalDAV failure (exception + FAILED status), send False/exception,
    PipelineCriticalError propagation, secret redaction with sentinel,
    empty agenda/homework, iCal cache cleanup
  - Secret redaction test uses mock (no network) and proves configured-secret
    propagation via non-URL sentinel in RuntimeError

Validation: 619 tests pass, ruff/mypy/bandit/pre-commit green, coverage 91%.

Co-authored-by: opencode/coder <coder@agents.invalid>
Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
2026-09-08 12:20:29 +02:00
26b083561a Ignore worktres 2026-09-08 11:50:25 +02:00
d7d31e14ff feat: orchestrer le pipeline M11
Co-authored-by: Codex/gpt-5.6-terra <codex-gpt-5-6-terra@agents.invalid>
2026-09-08 11:28:02 +02:00
be5beb45aa docs(M10): align GUIDE §10 and TODO.md with real slixmpp API and D6 contract
GUIDE_DEV_PYTHON.md §10 corrections:
- Fix XmppSettings defaults (host="", resource="pronote-sync")
- Replace Google-style docstrings with Sphinx/reST in examples
- Document connect(host, port) returning asyncio.Future, remove process() reference
- Document TLS mapping: use_tls=True → direct TLS, use_tls=False → STARTTLS
- Fix factory signature: get_channel(XmppSettings, dry_run) -> Channel | None
- Document Channel.send() -> bool never raises PipelineWarning (D6)
- Fix duplicate §10.3 numbering → §10.3-§10.6
- Remove pronote_messages duplication in M11 example
- Document JID with resource construction
- Document enriched message format (date, change types, times, due date, author)

TODO.md M10:
- Adjust acceptance criterion: channel returns False, pipeline emits PipelineWarning

.secrets.baseline:
- Line numbers updated for documentation shifts

Co-authored-by: opencode/tech-writer <tech-writer@agents.invalid>
m10-xmpp-channel
2026-09-08 10:16:44 +02:00
b2106e75ac fix(M10): apply FIXME_M10 corrections (transport, dry_run, format, security)
Fix all 8 findings from the independent review (FIXME_M10.md):

#1 Transport compatible with slixmpp 1.17.0 (D5):
  - Use real ClientXMPP type (remove Any), JID with resource
  - connect(host, port) explicit, no use_tls kwarg
  - enable_direct_tls/enable_starttls configured before connect
  - Single timeout via asyncio.Future for session_start/failed_auth/disconnected
  - Remove premature 'starttls' in features check, remove auto_reconnect
  - try/finally guarantees disconnect on all paths (#4)

#2 Factory dry_run no longer bypassed (D6):
  - Single send() entry point in SyncXmppChannel
  - dry_run check before any ClientXMPP creation
  - Remove XmppChannel.send() dual implementation

#3 Thread daemon removed — single asyncio.run(), documented limitation

#5 Richer message format:
  - Target date header, change type [Ajouté/Supprimé/Modifié]
  - Lesson times, homework due date, message author
  - No pronote_messages duplication (external_info = blog + other_info only)

#6 Error contract unified (D6):
  - Channel.send() -> bool never raises PipelineWarning
  - Errors logged with redaction, returns False
  - PipelineWarning(step='xmpp') will be created by pipeline M11

#7 Tests faithful to slixmpp 1.17.0 API:
  - FakeClientXMPP with real connect(host,port)/disconnect() signatures
  - Assertions on host, port, resource, mtype='chat'
  - No RuntimeWarning from unawaited coroutines

#8 .secrets.baseline restored from main

Coverage: 96.44% on channels/, 600 tests pass, pre-commit all-files green.

Co-authored-by: opencode/coder <coder@agents.invalid>
Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
2026-09-08 02:16:28 +02:00
b61d314b7f test: add M10 integration tests and coverage to 99% (M10-U7)
Integration tests validating the 3 M10 acceptance criteria end-to-end
with mocked slixmpp:
1. XmppChannel.send sends formatted direct message
2. XMPP error → PipelineWarning, no unhandled exception
3. No secret in XMPP logs (sentinel-based verification)

Additional unit tests covering daemon-thread branch of SyncXmppChannel,
deferred failed_auth, session timeout, disconnected handler, and
_format_message edge cases. Channels coverage: 99.18%.

All M10 checklist items marked complete in TODO.md.

Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
Co-authored-by: opencode/coder <coder@agents.invalid>
2026-09-08 00:58:40 +02:00
e07a6d709d feat: implement get_channel factory for XMPP channel (M10-U6)
get_channel(settings, dry_run=False) -> Channel | None with:
- enabled=False → None (no warning, no exception)
- enabled=True + missing jid/password/to/host → redacted warning log, None
- enabled=True + complete config → SyncXmppChannel instance
- Factory never raises exceptions (D2 non-blocking degradation)
- redact_secrets with extra_secrets=[password, jid, to] on warning logs

Re-exports Channel, XmppChannel, SyncXmppChannel from channels package.

19 unit tests covering disabled, misconfigured, complete, dry-run, and
secret-safe warning log scenarios.

Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
Co-authored-by: opencode/coder <coder@agents.invalid>
2026-09-07 23:29:25 +02:00
1962e13eba feat: implement XmppChannel and SyncXmppChannel (M10-U4+U5)
XmppChannel sends direct messages via slixmpp ClientXMPP with:
- _format_message: 5 emoji sections (synthèse, agenda, devoirs, messages, infos)
  with sanitize_plaintext on all content (SEC-XMPP-06)
- send_async: public async method with connect, STARTTLS verification,
  send_message, disconnect lifecycle (D1, SEC-XMPP-04)
- send: sync wrapper via asyncio.run() for direct callers

SyncXmppChannel adapts async XmppChannel for synchronous pipeline use (D4):
- asyncio.run() when no event loop running (nominal pipeline)
- daemon thread with timeout when event loop already running
- Returns False on any error, never raises (non-blocking)

Security:
- auto_reconnect=False, failed_auth → disconnect + PipelineWarning (SEC-XMPP-04)
- __cause__ and __context__ cleared on all PipelineWarning raises (SEC-XMPP-05)
- redact_secrets with extra_secrets=[jid, password, to] on all logs (SEC-XMPP-02)
- STARTTLS features check post-connection, disconnect on failure (D1)
- dry-run mode logs redacted message without connecting

28 unit tests (20 channel + 8 adapter) covering success, errors, security.

Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
Co-authored-by: opencode/coder <coder@agents.invalid>
2026-09-07 23:28:18 +02:00
dcf7f69c5a feat: add sanitize_plaintext for XMPP text sanitization (M10-U3)
Add sanitize_plaintext(text: str) -> str to utils/text.py for preparing
XMPP plain-text message bodies from untrusted Pronote/AI content.

- Strips HTML tags via BeautifulSoup (html.parser)
- Strips C0, DEL, and C1 control characters (preserves \t, \n, \r)
- Preserves Unicode including emojis (📌📅📚💬📢)
- Idempotent: f(f(x)) == f(x)
- Addresses SEC-XMPP-06: XMPP injection hardening

37 unit tests covering HTML, entities, control chars, emojis, idempotence.

Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
Co-authored-by: opencode/coder <coder@agents.invalid>
2026-09-07 21:04:13 +02:00
7d765476de feat: define Channel Protocol for output channels (M10-U2)
Add @runtime_checkable Channel Protocol with send(XmppMessage) -> bool
as the structural contract for all output channels (XMPP, future CalDAV, etc).

6 unit tests covering protocol structure, conforming/non-conforming classes,
method signature introspection, and bool return type.

Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
Co-authored-by: opencode/coder <coder@agents.invalid>
2026-09-07 21:04:01 +02:00
68a5d96c2a feat: add TLS policy and field constraints to XmppSettings (M10-U1)
Enforce TLS on non-loopback hosts via @field_validator on use_tls,
and add unconditional Field constraints on port (1-65535) and timeout (>0).

Security:
- use_tls=False rejected outside {localhost, 127.0.0.1, ::1} regardless of enabled
- field_validator on use_tls (not model_validator) prevents raw config leakage
- hide_input_in_errors=True as defense-in-depth
- Validation error messages contain no secrets (jid, password, recipient)

16 unit tests covering port/timeout bounds, TLS policy, loopback, secret safety.

Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
Co-authored-by: opencode/coder <coder@agents.invalid>
2026-09-07 21:01:48 +02:00
a5a8183663 feat: add PipelineWarning for non-blocking pipeline errors (M10-U0)
Add PipelineWarning(PronoteSyncError) to the canonical error hierarchy.
This non-blocking warning type is used by the XMPP channel (and future
channels) to signal recoverable failures without breaking the pipeline.

- PipelineWarning inherits from PronoteSyncError, not Warning builtin
- Constructor: (message, step=None) with recoverable=True
- 8 unit tests covering inheritance, raising, catching, attributes

Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
Co-authored-by: opencode/coder <coder@agents.invalid>
2026-09-07 21:01:36 +02:00
58c7fa147f merge: provider openai-compatible pour la synthèse IA (FEAT_M9) 2026-09-07 20:00:02 +02:00
6b9ab75977 docs: document openai-compatible provider and FIXME_M9 corrections
Update GUIDE_DEV_PYTHON.md, TODO.md, and AGENTS.md to reflect the
decisions and work done in the FEAT_M9 and FIXME_M9 sessions.

GUIDE_DEV_PYTHON.md:
- Header: add entry in recent updates
- 3.1.2: AI_PROVIDER now documents openai-compatible with
  Literal type; add AI_ALLOW_INSECURE_HTTP row; move decision
  block after table to fix rendering
- 3.2: AISettings code block updated with openai-compatible and
  allow_insecure_http field; decision note extended
- 3.1.3: .env.example adds OpenRouter (HTTPS) and Ollama (HTTP)
  examples, both commented

TODO.md:
- M9 factory line now mentions openai-compatible with URL validation
- Add FEAT_M9 and FIXME_M9 notes after M9 acceptance criteria

AGENTS.md:
- Section 5: new subsection for openai-compatible provider contract
  documenting validation rules, degraded mode, and security constraints

Co-authored-by: opencode/tech-writer anthropic.claude-sonnet-4-5 <anthropic.claude-sonnet-4-5@agents.invalid>
2026-09-07 19:59:13 +02:00
13e058f22c feat: add openai-compatible provider for custom AI endpoints
Add AI_PROVIDER=openai-compatible mode that reuses OpenAISynthesisProvider
with a validated custom base_url, allowing any OpenAI-compatible API
(OpenRouter, Ollama, LiteLLM proxy, etc.) without new code.

Configuration:
- AISettings.provider now accepts openai-compatible
- New AISettings.allow_insecure_http: bool = False (HTTP opt-in)
- .env.example: commented examples for OpenRouter (HTTPS) and Ollama (HTTP)

Factory validation (_validate_openai_compatible_config):
- base_url and model required, api_key required (MVP)
- HTTPS enforced unless allow_insecure_http=true
- Credentials in URL rejected, sensitive query params rejected
  (including valueless params via keep_blank_values=True)
- Malformed URLs and missing hostname rejected (ValueError caught)
- No /v1 manipulation; degraded to None + warning on invalid config
- redact_url() used for all URL warnings

Tests: 13 new factory tests in test_synthesis.py covering routing,
URL validation, HTTP policy, credentials, sentinel non-leak, no-network.
Coverage: 91.57% (synthesis module).

Docs: GUIDE_DEV_PYTHON.md §9.5 updated with 3-provider table, validation
rules, and synchronized code example.

mypy override for openai.* (follow_imports=skip) to work around
mypy 2.3.1 internal error in pre-commit's isolated environment.

Co-authored-by: opencode/coder anthropic.claude-sonnet-4-5 <anthropic.claude-sonnet-4-5@agents.invalid>
Co-authored-by: opencode/test-engineer anthropic.claude-sonnet-4-5 <anthropic.claude-sonnet-4-5@agents.invalid>
Co-authored-by: opencode/tech-writer anthropic.claude-sonnet-4-5 <anthropic.claude-sonnet-4-5@agents.invalid>
2026-09-07 19:44:40 +02:00
2a27225fa0 merge: corrections d'audit FIXME_M9 dans la synthèse IA
Correctifs FIXME_M9 : redact_secrets étendue (extra_secrets), clés en
SecretStr, contenu des messages dans le prompt, validation de sortie
(emoji/titre/liste/HTML), tests litellm robustes (importorskip), .env.example
désactivé, documentation §9.2-§9.5 alignée.

Co-authored-by: opencode/coder <coder@agents.invalid>
Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
Co-authored-by: opencode/tech-writer <tech-writer@agents.invalid>
2026-09-07 19:02:55 +02:00
19cbf8f13f fix(M9): corrections d'audit FIXME_M9 — secrets, messages, validation, tests
Cinq corrections de l'audit FIXME_M9 :
- redact_secrets() étendue avec extra_secrets pour masquer les clés brutes ;
  providers stockent SecretStr jusqu'à l'appel SDK.
- _build_prompt() inclut le contenu des messages (tronqué à 500 car.) ;
  prompt système renforcé contre l'injection.
- _validate_output() supprime les emojis et rejette titre/liste/HTML → None.
- Tests litellm utilisent importorskip + LITELLM_LOCAL_MODEL_COST_MAP=true.
- .env.example désactive l'IA par défaut (AI_ENABLED=false).
- Documentation §9.2-§9.5 alignée avec l'implémentation (SDK openai, SecretStr,
  factory réelle, validation sortie, politique hors réseau).

Co-authored-by: opencode/coder <coder@agents.invalid>
Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
Co-authored-by: opencode/tech-writer <tech-writer@agents.invalid>
2026-09-07 19:01:25 +02:00
4b0e2858a6 merge: jalon M9 — synthèse IA (providers OpenAI/litellm, factory, tests)
M9 livré : protocole SynthesisProvider, OpenAISynthesisProvider (SDK
openai, prompt FR, mode dégradé strict), LiteLLMSynthesisProvider
(extra optionnel), factory get_synthesis_provider(). 23 tests sans
réseau, couverture synthesis/ 93%.

Co-authored-by: opencode/coder <coder@agents.invalid>
Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
2026-09-07 17:08:05 +02:00
775b5ae9cc docs: marquer le jalon M9 (synthèse IA) comme terminé dans TODO.md
Co-authored-by: opencode/coder <coder@agents.invalid>
2026-09-07 17:07:47 +02:00
92833060e2 feat(M9): synthèse IA — protocole, providers OpenAI/litellm, factory, tests
Synthèse optionnelle via SDK openai (client injectable, prompt système
FR, max 800 car., timeout 30 s, temp 0.3). Mode dégradé strict :
generate() ne lève jamais, retourne None si clé absente/timeout/erreur.
Provider litellm optionnel (extra ai-litellm) réutilisant le prompt
OpenAI. Factory get_synthesis_provider() selon AISettings. 23 tests
sans réseau, couverture synthesis/ 93%.

Co-authored-by: opencode/coder <coder@agents.invalid>
Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
2026-09-07 17:07:05 +02:00
4d11ec9b22 merge: jalon M8 — comparaison avec l'agenda théorique + corrections FIXME_M8
M8 livré : AgendaComparator dans sync/diff.py avec matching déterministe,
tolérance ±15 min, normalisation NFKC des matières, appariement un-à-un.
Correctifs FIXME_M8 : appariement consommé, filtrage par date, détails
triés déterministes, validateur AgendaChange strict, secondes à la minute
près, documentation §8.4/§8.5 alignée.

Co-authored-by: opencode/coder <coder@agents.invalid>
Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
Co-authored-by: opencode/tech-writer <tech-writer@agents.invalid>
2026-09-07 16:00:00 +02:00
5907c9aeaf fix(M8): corrections d'audit FIXME_M8 — appariement, date, déterminisme, validateur
Quatre corrections bloquantes/majeures de l'audit FIXME_M8 :
- Appariement un-à-un déterministe (consommation du candidat sélectionné) ;
  1 réel / 2 théoriques → 1 REMOVED, 2 réels / 1 théorique → 1 ADDED.
- Filtrage strict par date : les cours réels hors target_date sont exclus
  du matching avec un warning logé (décision architecte : pas d'exception).
- Déterminisme des détails : formatage via sorted(set(...)) au lieu de
  set(...) brut, indépendant de PYTHONHASHSEED.
- Validateur AgendaChange strict : ADDED = lesson seule, REMOVED =
  theoretical_lesson seule, MODIFIED = les deux requis.
- Comparaison à la minute près dans _is_modified (cohérent avec _matches).
- Documentation §8.4/§8.5 alignée avec l'implémentation (tolérance 15 min,
  API compare(), normalize_subject référencé, appariement consommé).

Co-authored-by: opencode/coder <coder@agents.invalid>
Co-authored-by: opencode/tech-writer <tech-writer@agents.invalid>
2026-09-07 15:59:17 +02:00
d2cf59c713 docs: marquer le jalon M8 (comparaison agenda théorique) comme terminé
M8 livré : AgendaComparator dans sync/diff.py avec matching déterministe,
tolérance ±15 min, normalisation NFKC des matières, REMOVED par existence.
Le critère d'acceptation 3 (absence de THEORETICAL_AGENDA_PATH) est couvert
par design et reporté à M11 (composition root).

Co-authored-by: opencode/coder <coder@agents.invalid>
2026-09-07 13:58:25 +02:00
093253a41c test(M8): tests unitaires pour AgendaComparator (17 cas)
Couvre : agendas vides, ADDED/REMOVED/MODIFIED, tolérance ±15 min
(bord inclusif), normalisation NFKC des matières, matching multi-candidats
par plus petit id, comparaison ordre-insensible des enseignants/salles,
statut != NORMAL, REMOVED par existence (pas par sélection), ordre
déterministe et idempotence. Couverture de sync/diff.py : 92%.

Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
2026-09-07 13:50:56 +02:00
10e5f22501 feat(M8): comparateur d'agenda (AgendaComparator) dans sync/diff.py
Comparaison déterministe entre l'agenda réel (Lesson) et l'agenda
théorique (TheoreticalLesson) produisant un AgendaDiff (ADDED/REMOVED/
MODIFIED). Matching par jour + tolérance ±15 min symétrique + matière
normalisée ; tri des candidats par id stable. REMOVED par existence
(non-appariement), pas par sélection. Comparaison ordre-insensible des
enseignants et salles via set(). Détection MODIFIED incluant les horaires,
la matière, les enseignants, les salles et le statut.

Co-authored-by: opencode/coder <coder@agents.invalid>
2026-09-07 13:39:22 +02:00
557555c65b refactor: déplacer normalize_subject vers utils/text.py avec ré-export
La normalisation des matières (NFKC + espaces + ponctuation + minuscules)
est désormais dans pronote_sync/utils/text.py pour permettre son partage
entre sources/theoretical/file.py et sync/diff.py (M8) sans couplage de
couche. L'import depuis file.py est préservé par ré-export explicite.

Co-authored-by: opencode/coder <coder@agents.invalid>
2026-09-07 13:30:15 +02:00
c309bcbb64 docs: marquer le jalon M7 (synchronisation CalDAV) comme terminé dans TODO.md 2026-09-07 13:18:04 +02:00
88a75cd162 Ignore zvec-grep 2026-09-07 12:29:17 +02:00