82 Commits

Author SHA1 Message Date
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
91f0b6d9a0 merge: jalon M7 — synchronisation différentielle CalDAV 2026-09-07 12:26:06 +02:00
a1bae41be8 fix(M7): corrections d'audit FIXME_M7 — sécurité, fenêtre, UID, timezone
Corrige les 5 constats de l'audit FIXME_M7 :

#1 (Bloquant) — Protection des événements non marqués :
- upsert_event() vérifie le marqueur X-PRONOTE-SYNC-MANAGED avant
  modification ; lève PronoteSyncError en cas de collision avec un
  événement non géré (aucune écriture)
- delete_event() vérifie le marqueur ; no-op avec warning si non géré
- Méthode privée _is_managed_event() factorisant le contrôle

#2 (Bloquant) — Fenêtre de synchronisation :
- Calcul en journées entières (minuit à minuit exclusif)
- Filtrage des données locales (lessons, homeworks, school_events) avant
  passage au planner
- Paramètre now injectable pour les tests

#3 (Bloquant) — UID canonique vs brut :
- list_managed_events() retourne (raw_uid, canonical_uid, vevent)
- compute_plan() matche par UID canonique, route les raw UID vers
  *_to_remove, retourne le mapping remote_raw_by_canonical
- executor.execute() utilise le raw UID pour les mises à jour (pas de
  doublon)
- Pas de migration destructive des UID distants existants

#4 (Correction) — Normalisation temporelle UTC :
- normalize_datetime_to_utc() dans utils/uid.py : naïve → Europe/Paris →
  UTC ; consciente → UTC
- Utilisée par generate_deterministic_uid() et component_to_signature()
- Deux représentations du même instant → même UID et même signature

#5 (Compatibilité) — date_search déprécié :
- Remplacement par calendar.search(start, end, event=True, expand=True)

Documentation :
- GUIDE_DEV_PYTHON.md : suppression des références obsolètes à
  sync/state.py et état SQLite/JSON ; mise à jour de l'API CalDAV
  (search au lieu de date_search, upsert par UID)
- TODO.md : M7 décoché (corrections en cours de validation)

Tests : 390 passés, couverture 95.61%

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 12:24:22 +02:00
b4b0247919 feat(M7): synchronisation différentielle CalDAV
Implémente la synchronisation des événements Pronote vers un calendrier
CalDAV (Nextcloud) de façon idempotente et sécurisée.

Production :
- sync/serialization.py : sérialisation Lesson/Homework/SchoolEvent vers
  VEVENT, signature sémantique (exclut DTSTAMP/CREATED/LAST-MODIFIED),
  enveloppe VCALENDAR complète avec VERSION:2.0 et PRODID
- sync/caldav.py : passerelle CalDAV isolant caldav>=1.3.0, résolution du
  calendrier via principal().calendars() avec boundary matching, upsert par
  UID (fetch-then-save), exceptions expurgées et __context__ propre, mot de
  passe non stocké en clair, context manager
- sync/planner.py : calcul explicite du CalDAVSyncPlan (add/update/remove
  par comparaison de signatures sémantiques, routage par préfixe d'UID)
- sync/executor.py : exécution du plan avec dry-run (aucune écriture),
  isolation des erreurs par événement, statut FAILED/SKIPPED/SUCCESS
- sync/synchronizer.py : orchestration en trois phases (scan, plan,
  exécution), SKIPPED si CalDAV non configuré
- sync/__init__.py : export synchronize()
- sources/pronote/client.py : normalisation UID via normalize_pronote_uid/
  generate_deterministic_uid (parité avec ical.py)
- config/settings.py : CalDAVSettings durci (url SecretStr, validation
  HTTPS, allow_insecure_http pour localhost, serializer redact_url)

Tests (381 passés, couverture 95.58%) :
- tests/unit/test_sync_serialization.py (21 tests)
- tests/unit/test_caldav_planner.py (16 tests)
- tests/unit/test_caldav_executor.py (18 tests)
- tests/unit/test_caldav_gateway.py (24 tests)
- tests/unit/test_caldav_security.py (18 tests)
- tests/unit/test_uid_equivalence.py (8 tests)
- tests/integration/test_caldav_sync.py (11 tests, faux serveur en mémoire)
- tests/conftest.py : fixtures partagées

Documentation :
- GUIDE_DEV_PYTHON.md §7 : API réelle caldav>=1.3.0, principal().calendars(),
  VCALENDAR complet, upsert par UID, pas d'état local, événements non gérés
  protégés, CalDAVSettings durci (SecretStr, HTTPS, allow_insecure_http)
- TODO.md : M7 coché
- .env.example : CALDAV_ALLOW_INSECURE_HTTP=false

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 09:24:18 +02:00
ebbe39f1f0 docs(M7): aligner guide, TODO et pre-commit pour la sync CalDAV
Met à jour la documentation et la configuration pour le jalon M7 selon
les décisions d'architecture :

- GUIDE_DEV_PYTHON.md §7 : API réelle caldav>=1.3.0 (pas le pseudo-code),
  calendar_path (pas calendar_name), plan CalDAVSyncPlan explicite avant
  exécution, pas d'état local (scan distant), événements non marqués
  jamais modifiés
- TODO.md M7 : suppression de sync/state.py et BlogRSSState, ajout de
  l'exécution du plan et de la protection des événements non gérés
- .pre-commit-config.yaml : caldav>=1.3.0 ajouté aux additional_dependencies
  du hook mypy

Co-authored-by: opencode/coder <coder@agents.invalid>
Co-authored-by: opencode/tech-writer <tech-writer@agents.invalid>
2026-09-07 00:00:54 +02:00
958bb3ec5d merge: corrections d'audit FIXME_M6 dans l'agenda théorique
Intègre les corrections de la relecture indépendante :
- Normalisation des matières (NFKC + espaces + ponctuation + casse)
- Expurgation du secret dans l'erreur de collision d'IDs
- Tests renforcés (parité isolée, normalisation, non-fuite)
- Documentation alignée (TODO coché, guide §8.4 corrigé)

Co-authored-by: opencode/coder <coder@agents.invalid>
Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
2026-09-06 23:31:25 +02:00