methodic-research 0.40.0__tar.gz → 0.41.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. {methodic_research-0.40.0/src/methodic_research.egg-info → methodic_research-0.41.0}/PKG-INFO +1 -1
  2. {methodic_research-0.40.0 → methodic_research-0.41.0}/pyproject.toml +1 -1
  3. {methodic_research-0.40.0 → methodic_research-0.41.0}/src/methodic/experiments.py +150 -0
  4. {methodic_research-0.40.0 → methodic_research-0.41.0/src/methodic_research.egg-info}/PKG-INFO +1 -1
  5. {methodic_research-0.40.0 → methodic_research-0.41.0}/tests/test_experiments.py +65 -0
  6. {methodic_research-0.40.0 → methodic_research-0.41.0}/LICENSE +0 -0
  7. {methodic_research-0.40.0 → methodic_research-0.41.0}/README.md +0 -0
  8. {methodic_research-0.40.0 → methodic_research-0.41.0}/setup.cfg +0 -0
  9. {methodic_research-0.40.0 → methodic_research-0.41.0}/src/methodic/__init__.py +0 -0
  10. {methodic_research-0.40.0 → methodic_research-0.41.0}/src/methodic/activity.py +0 -0
  11. {methodic_research-0.40.0 → methodic_research-0.41.0}/src/methodic/api_keys.py +0 -0
  12. {methodic_research-0.40.0 → methodic_research-0.41.0}/src/methodic/assets.py +0 -0
  13. {methodic_research-0.40.0 → methodic_research-0.41.0}/src/methodic/chronicle.py +0 -0
  14. {methodic_research-0.40.0 → methodic_research-0.41.0}/src/methodic/cli.py +0 -0
  15. {methodic_research-0.40.0 → methodic_research-0.41.0}/src/methodic/collections.py +0 -0
  16. {methodic_research-0.40.0 → methodic_research-0.41.0}/src/methodic/datasets.py +0 -0
  17. {methodic_research-0.40.0 → methodic_research-0.41.0}/src/methodic/errata.py +0 -0
  18. {methodic_research-0.40.0 → methodic_research-0.41.0}/src/methodic/errors.py +0 -0
  19. {methodic_research-0.40.0 → methodic_research-0.41.0}/src/methodic/feed.py +0 -0
  20. {methodic_research-0.40.0 → methodic_research-0.41.0}/src/methodic/feedback.py +0 -0
  21. {methodic_research-0.40.0 → methodic_research-0.41.0}/src/methodic/imports.py +0 -0
  22. {methodic_research-0.40.0 → methodic_research-0.41.0}/src/methodic/me.py +0 -0
  23. {methodic_research-0.40.0 → methodic_research-0.41.0}/src/methodic/pending.py +0 -0
  24. {methodic_research-0.40.0 → methodic_research-0.41.0}/src/methodic/publications.py +0 -0
  25. {methodic_research-0.40.0 → methodic_research-0.41.0}/src/methodic/reports.py +0 -0
  26. {methodic_research-0.40.0 → methodic_research-0.41.0}/src/methodic/research_prompts.py +0 -0
  27. {methodic_research-0.40.0 → methodic_research-0.41.0}/src/methodic/runs.py +0 -0
  28. {methodic_research-0.40.0 → methodic_research-0.41.0}/src/methodic/search.py +0 -0
  29. {methodic_research-0.40.0 → methodic_research-0.41.0}/src/methodic/tags.py +0 -0
  30. {methodic_research-0.40.0 → methodic_research-0.41.0}/src/methodic/transport.py +0 -0
  31. {methodic_research-0.40.0 → methodic_research-0.41.0}/src/methodic/types.py +0 -0
  32. {methodic_research-0.40.0 → methodic_research-0.41.0}/src/methodic/upload_tracker.py +0 -0
  33. {methodic_research-0.40.0 → methodic_research-0.41.0}/src/methodic/variations.py +0 -0
  34. {methodic_research-0.40.0 → methodic_research-0.41.0}/src/methodic_research.egg-info/SOURCES.txt +0 -0
  35. {methodic_research-0.40.0 → methodic_research-0.41.0}/src/methodic_research.egg-info/dependency_links.txt +0 -0
  36. {methodic_research-0.40.0 → methodic_research-0.41.0}/src/methodic_research.egg-info/entry_points.txt +0 -0
  37. {methodic_research-0.40.0 → methodic_research-0.41.0}/src/methodic_research.egg-info/requires.txt +0 -0
  38. {methodic_research-0.40.0 → methodic_research-0.41.0}/src/methodic_research.egg-info/top_level.txt +0 -0
  39. {methodic_research-0.40.0 → methodic_research-0.41.0}/tests/test_activity.py +0 -0
  40. {methodic_research-0.40.0 → methodic_research-0.41.0}/tests/test_api_keys.py +0 -0
  41. {methodic_research-0.40.0 → methodic_research-0.41.0}/tests/test_assets.py +0 -0
  42. {methodic_research-0.40.0 → methodic_research-0.41.0}/tests/test_cli.py +0 -0
  43. {methodic_research-0.40.0 → methodic_research-0.41.0}/tests/test_client.py +0 -0
  44. {methodic_research-0.40.0 → methodic_research-0.41.0}/tests/test_collections.py +0 -0
  45. {methodic_research-0.40.0 → methodic_research-0.41.0}/tests/test_config.py +0 -0
  46. {methodic_research-0.40.0 → methodic_research-0.41.0}/tests/test_datasets.py +0 -0
  47. {methodic_research-0.40.0 → methodic_research-0.41.0}/tests/test_distill.py +0 -0
  48. {methodic_research-0.40.0 → methodic_research-0.41.0}/tests/test_feed.py +0 -0
  49. {methodic_research-0.40.0 → methodic_research-0.41.0}/tests/test_feedback.py +0 -0
  50. {methodic_research-0.40.0 → methodic_research-0.41.0}/tests/test_imports.py +0 -0
  51. {methodic_research-0.40.0 → methodic_research-0.41.0}/tests/test_pending.py +0 -0
  52. {methodic_research-0.40.0 → methodic_research-0.41.0}/tests/test_pending_reasons.py +0 -0
  53. {methodic_research-0.40.0 → methodic_research-0.41.0}/tests/test_publications.py +0 -0
  54. {methodic_research-0.40.0 → methodic_research-0.41.0}/tests/test_reports.py +0 -0
  55. {methodic_research-0.40.0 → methodic_research-0.41.0}/tests/test_research_prompts.py +0 -0
  56. {methodic_research-0.40.0 → methodic_research-0.41.0}/tests/test_runs.py +0 -0
  57. {methodic_research-0.40.0 → methodic_research-0.41.0}/tests/test_search.py +0 -0
  58. {methodic_research-0.40.0 → methodic_research-0.41.0}/tests/test_upload_tracker.py +0 -0
  59. {methodic_research-0.40.0 → methodic_research-0.41.0}/tests/test_variations.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: methodic-research
3
- Version: 0.40.0
3
+ Version: 0.41.0
4
4
  Summary: Python client for the Chronicle experiment platform
5
5
  License-Expression: Apache-2.0
6
6
  Project-URL: Documentation, https://docs.methodiclabs.ai
@@ -8,7 +8,7 @@ name = "methodic-research"
8
8
  # methodic-lib-publish.yml overwrites this from versions.toml at build
9
9
  # time, so the published artifact has a single source of truth. Kept
10
10
  # here for local `pip install -e conductor/`.
11
- version = "0.40.0"
11
+ version = "0.41.0"
12
12
  description = "Python client for the Chronicle experiment platform"
13
13
  requires-python = ">=3.11"
14
14
  license = "Apache-2.0"
@@ -268,6 +268,146 @@ class ExperimentsAPI:
268
268
  payload["source_asset_id"] = source_asset_id
269
269
  return self._t.post(f"/experiments/{experiment_id}/findings", json=payload)
270
270
 
271
+ def record_lesson(
272
+ self,
273
+ experiment_id: str,
274
+ *,
275
+ title: str,
276
+ body_md: str,
277
+ origin: str,
278
+ category: str | None = None,
279
+ variation: int | None = None,
280
+ evidence: dict[str, Any] | None = None,
281
+ ) -> dict[str, Any]:
282
+ """Record a research lesson — a durable corrected assumption — onto
283
+ the experiment (designs/research-lessons.md).
284
+
285
+ Record one when you have repeatedly made the same bad assumption,
286
+ when the researcher explicitly or implicitly corrects a wrong
287
+ premise, or on request. List existing lessons first and refine
288
+ (:meth:`update_lesson`) instead of duplicating. Lessons surface on
289
+ the experiment's Lessons tab, are injected into every future
290
+ agent's context (this experiment + descendants), and emit a
291
+ ``lesson.recorded`` feed event. Requires ``Write`` on the
292
+ experiment. (MCP-native agents have the same surface as the
293
+ ``chronicle.record_lesson`` tool.)
294
+
295
+ Args:
296
+ experiment_id: Target experiment.
297
+ title: One line, imperative where possible.
298
+ body_md: The lesson (Markdown, self-contained): wrong
299
+ assumption, correction, evidence, what to do instead.
300
+ origin: ``"repeated_error"`` | ``"researcher_correction"`` |
301
+ ``"self_identified"`` | ``"manual"``.
302
+ category: ``"assumption"`` (default) | ``"methodology"`` |
303
+ ``"environment"`` | ``"data"`` | ``"other"``.
304
+ variation: The variation the lesson arose from, when
305
+ variation-specific.
306
+ evidence: Pointers only (session shard / run / report ids) —
307
+ never content.
308
+
309
+ Returns the recorded lesson.
310
+ """
311
+ payload: dict[str, Any] = {
312
+ "title": title,
313
+ "body_md": body_md,
314
+ "origin": origin,
315
+ }
316
+ if category is not None:
317
+ payload["category"] = category
318
+ if variation is not None:
319
+ payload["variation"] = variation
320
+ if evidence is not None:
321
+ payload["evidence"] = evidence
322
+ return self._t.post(f"/experiments/{experiment_id}/lessons", json=payload)
323
+
324
+ def list_lessons(
325
+ self,
326
+ experiment_id: str,
327
+ *,
328
+ status: str | None = None,
329
+ category: str | None = None,
330
+ include_ancestors: bool = True,
331
+ ) -> list[dict[str, Any]]:
332
+ """List the experiment's research lessons — the corrected
333
+ assumptions every conclusion and proposal must be checked against.
334
+
335
+ Defaults to active lessons **including** those inherited from
336
+ ancestor experiments (each stamped ``inherited_from``). Consult
337
+ this before writing conclusions or proposing variations.
338
+ Requires ``Read`` on the experiment.
339
+
340
+ Args:
341
+ experiment_id: Target experiment.
342
+ status: ``"active"`` (default) | ``"retired"`` | ``"all"`` —
343
+ own-lesson filter; inherited lessons are always
344
+ active-only.
345
+ category: Optional category filter.
346
+ include_ancestors: Include ancestor experiments' active
347
+ lessons (default True).
348
+
349
+ Returns the lesson list.
350
+ """
351
+ params: dict[str, Any] = {}
352
+ if status is not None:
353
+ params["status"] = status
354
+ if category is not None:
355
+ params["category"] = category
356
+ if include_ancestors:
357
+ params["include_ancestors"] = "true"
358
+ resp = self._t.get(
359
+ f"/experiments/{experiment_id}/lessons", params=params or None
360
+ )
361
+ return resp.get("lessons", [])
362
+
363
+ def update_lesson(
364
+ self,
365
+ experiment_id: str,
366
+ lesson_id: str,
367
+ *,
368
+ title: str | None = None,
369
+ body_md: str | None = None,
370
+ category: str | None = None,
371
+ variation: int | None = None,
372
+ evidence: dict[str, Any] | None = None,
373
+ ) -> dict[str, Any]:
374
+ """Refine an existing lesson in place (the dedup contract) — absent
375
+ fields are unchanged; retired lessons are immutable (409).
376
+ Requires ``Write`` on the experiment. Returns the updated lesson."""
377
+ payload: dict[str, Any] = {}
378
+ if title is not None:
379
+ payload["title"] = title
380
+ if body_md is not None:
381
+ payload["body_md"] = body_md
382
+ if category is not None:
383
+ payload["category"] = category
384
+ if variation is not None:
385
+ payload["variation"] = variation
386
+ if evidence is not None:
387
+ payload["evidence"] = evidence
388
+ return self._t.put(
389
+ f"/experiments/{experiment_id}/lessons/{lesson_id}", json=payload
390
+ )
391
+
392
+ def retire_lesson(
393
+ self,
394
+ experiment_id: str,
395
+ lesson_id: str,
396
+ *,
397
+ reason: str,
398
+ superseded_by: str | None = None,
399
+ ) -> dict[str, Any]:
400
+ """Retire a lesson that turned out wrong or obsolete (``active`` →
401
+ ``retired``; retire-don't-delete). Only lessons owned by this
402
+ experiment — inherited ones are retired at their home experiment.
403
+ Requires ``Write`` on the experiment. Returns the retired lesson."""
404
+ payload: dict[str, Any] = {"reason": reason}
405
+ if superseded_by is not None:
406
+ payload["superseded_by"] = superseded_by
407
+ return self._t.put(
408
+ f"/experiments/{experiment_id}/lessons/{lesson_id}/retire", json=payload
409
+ )
410
+
271
411
  def distill(
272
412
  self,
273
413
  experiment_id: str,
@@ -674,6 +814,16 @@ class Experiment:
674
814
  self._detail = None
675
815
  return summary_doc
676
816
 
817
+ def record_lesson(self, **kwargs: Any) -> dict[str, Any]:
818
+ """Record a research lesson on this experiment. Proxies to
819
+ :meth:`ExperimentsAPI.record_lesson`."""
820
+ return self._chronicle.experiments.record_lesson(self.id, **kwargs)
821
+
822
+ def list_lessons(self, **kwargs: Any) -> list[dict[str, Any]]:
823
+ """List this experiment's research lessons (own + inherited by
824
+ default). Proxies to :meth:`ExperimentsAPI.list_lessons`."""
825
+ return self._chronicle.experiments.list_lessons(self.id, **kwargs)
826
+
677
827
  def distill(
678
828
  self,
679
829
  *,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: methodic-research
3
- Version: 0.40.0
3
+ Version: 0.41.0
4
4
  Summary: Python client for the Chronicle experiment platform
5
5
  License-Expression: Apache-2.0
6
6
  Project-URL: Documentation, https://docs.methodiclabs.ai
@@ -937,3 +937,68 @@ def test_create_passes_title(requests_mock):
937
937
  body = requests_mock.last_request.json()
938
938
  assert body["title"] == "Display title"
939
939
  assert body["hypothesis_summary"] == "h"
940
+
941
+
942
+ # --- Research lessons (designs/research-lessons.md) ---
943
+
944
+ def test_record_lesson_posts_body(requests_mock):
945
+ requests_mock.post(
946
+ f"{BASE}/v1/experiments/{EXP}/lessons",
947
+ json={"id": "les-1", "title": "check the mask", "status": "active"},
948
+ )
949
+
950
+ lesson = _chronicle().experiments.record_lesson(
951
+ EXP,
952
+ title="check the mask",
953
+ body_md="The boundary mask is inverted in this dataset.",
954
+ origin="researcher_correction",
955
+ category="data",
956
+ variation=4,
957
+ evidence={"run": 3},
958
+ )
959
+
960
+ assert lesson["id"] == "les-1"
961
+ sent = requests_mock.last_request.json()
962
+ assert sent == {
963
+ "title": "check the mask",
964
+ "body_md": "The boundary mask is inverted in this dataset.",
965
+ "origin": "researcher_correction",
966
+ "category": "data",
967
+ "variation": 4,
968
+ "evidence": {"run": 3},
969
+ }
970
+
971
+
972
+ def test_list_lessons_defaults_include_ancestors(requests_mock):
973
+ requests_mock.get(
974
+ f"{BASE}/v1/experiments/{EXP}/lessons",
975
+ json={"lessons": [{"id": "les-1"}, {"id": "les-2", "inherited_from": "parent-1"}]},
976
+ )
977
+
978
+ lessons = _chronicle().experiments.list_lessons(EXP)
979
+
980
+ assert [l["id"] for l in lessons] == ["les-1", "les-2"]
981
+ assert requests_mock.last_request.qs.get("include_ancestors") == ["true"]
982
+
983
+
984
+ def test_update_and_retire_lesson_put_bodies(requests_mock):
985
+ requests_mock.put(
986
+ f"{BASE}/v1/experiments/{EXP}/lessons/les-1",
987
+ json={"id": "les-1", "body_md": "sharper"},
988
+ )
989
+ updated = _chronicle().experiments.update_lesson(EXP, "les-1", body_md="sharper")
990
+ assert updated["body_md"] == "sharper"
991
+ assert requests_mock.last_request.json() == {"body_md": "sharper"}
992
+
993
+ requests_mock.put(
994
+ f"{BASE}/v1/experiments/{EXP}/lessons/les-1/retire",
995
+ json={"id": "les-1", "status": "retired"},
996
+ )
997
+ retired = _chronicle().experiments.retire_lesson(
998
+ EXP, "les-1", reason="fixed upstream", superseded_by="les-2"
999
+ )
1000
+ assert retired["status"] == "retired"
1001
+ assert requests_mock.last_request.json() == {
1002
+ "reason": "fixed upstream",
1003
+ "superseded_by": "les-2",
1004
+ }