foundry-implementation-actor 0.2.0__tar.gz → 0.2.1__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 (40) hide show
  1. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.2.1}/CLAUDE.md +9 -1
  2. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.2.1}/PKG-INFO +11 -1
  3. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.2.1}/README.md +10 -0
  4. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.2.1}/pyproject.toml +1 -1
  5. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.2.1}/src/foundry_implementation_actor/__init__.py +2 -1
  6. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.2.1}/src/foundry_implementation_actor/cli.py +11 -1
  7. foundry_implementation_actor-0.2.1/src/foundry_implementation_actor/conformance.py +119 -0
  8. foundry_implementation_actor-0.2.1/tests/fixtures/valid/actor-data.yaml +38 -0
  9. foundry_implementation_actor-0.2.1/tests/fixtures/valid/actor-message.yaml +17 -0
  10. foundry_implementation_actor-0.2.1/tests/fixtures/valid/actor-synchronous-messaging.yaml +21 -0
  11. foundry_implementation_actor-0.2.1/tests/fixtures/valid/actor.yaml +8 -0
  12. foundry_implementation_actor-0.2.1/tests/test_conformance.py +111 -0
  13. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.2.1}/uv.lock +1 -1
  14. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.2.1}/.github/workflows/ci.yml +0 -0
  15. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.2.1}/.github/workflows/release.yml +0 -0
  16. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.2.1}/.gitignore +0 -0
  17. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.2.1}/adr/ADR-FIA-0001-the-machinery-leaves-the-capability.md +0 -0
  18. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.2.1}/adr/README.md +0 -0
  19. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.2.1}/adr/template.md +0 -0
  20. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.2.1}/scripts/probe_grounding.py +0 -0
  21. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.2.1}/src/foundry_implementation_actor/cards/actor-data.yaml +0 -0
  22. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.2.1}/src/foundry_implementation_actor/cards/actor-message.yaml +0 -0
  23. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.2.1}/src/foundry_implementation_actor/cards/actor-synchronous-messaging.yaml +0 -0
  24. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.2.1}/src/foundry_implementation_actor/cards/actor.yaml +0 -0
  25. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.2.1}/src/foundry_implementation_actor/config.py +0 -0
  26. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.2.1}/src/foundry_implementation_actor/correlation.py +0 -0
  27. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.2.1}/src/foundry_implementation_actor/engine.py +0 -0
  28. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.2.1}/src/foundry_implementation_actor/grounding.py +0 -0
  29. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.2.1}/src/foundry_implementation_actor/handler.py +0 -0
  30. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.2.1}/src/foundry_implementation_actor/schemas/agentic-context.schema.yaml +0 -0
  31. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.2.1}/tests/conftest.py +0 -0
  32. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.2.1}/tests/fixtures/broken/actor-agentic-context.yaml +0 -0
  33. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.2.1}/tests/fixtures/valid/actor-agentic-context.yaml +0 -0
  34. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.2.1}/tests/test_cards.py +0 -0
  35. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.2.1}/tests/test_cli.py +0 -0
  36. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.2.1}/tests/test_config.py +0 -0
  37. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.2.1}/tests/test_engine.py +0 -0
  38. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.2.1}/tests/test_grounding.py +0 -0
  39. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.2.1}/tests/test_handler.py +0 -0
  40. {foundry_implementation_actor-0.2.0 → foundry_implementation_actor-0.2.1}/tests/test_portability.py +0 -0
@@ -29,7 +29,7 @@ There is no separate lint/format command configured in this repo.
29
29
 
30
30
  ## Architecture
31
31
 
32
- Six modules under `src/foundry_implementation_actor/`:
32
+ Seven modules under `src/foundry_implementation_actor/`:
33
33
 
34
34
  - **`config.py`** — `CapabilityConfig`. The heart. Loads the sidecar and derives **every**
35
35
  rendering of the capability id from two declared fields (`capability`, `source_repo`). Also
@@ -44,6 +44,10 @@ Six modules under `src/foundry_implementation_actor/`:
44
44
  opens a pull request.
45
45
  - **`correlation.py`** — the two ids and the step vocabulary. Moved **verbatim** from the repo this
46
46
  was extracted from; its code below the docstring is byte-identical.
47
+ - **`conformance.py`** — `check(folder)`. Compares a use's cards against the definition's, on
48
+ the derived wire contract only (door ids, each door's request/completion schema and engine).
49
+ Prose and `actor.yaml`'s `name:` are deliberately not compared — a use should name its own
50
+ capability. `lint` runs it beside the sidecar gate.
47
51
  - **`cli.py`** — argparse wiring only, no logic of its own.
48
52
 
49
53
  Beside them, two folders of committed contract, both shipped in the wheel:
@@ -56,6 +60,10 @@ Beside them, two folders of committed contract, both shipped in the wheel:
56
60
 
57
61
  ## Core invariants that any change must preserve
58
62
 
63
+ - **A use's cards are a hand copy, and hand copies drift.** Both folders pass `lint-card`
64
+ independently, and neither gate looks at the other — so a definition that gains a field leaves
65
+ every un-updated use linting green and refusing callers at runtime. `conformance.check` is the
66
+ only thing that catches it. Compare the DERIVED contract, never the prose.
59
67
  - **The `-actor` suffix is a claim, and it is checked.** `ADR-ECO-0022`: a package ending in
60
68
  `-actor` asserts a `papeete-actor` underneath, and one that ships no conformant card is
61
69
  misnamed. `tests/test_cards.py` runs `lint-card` on `cards/` in the suite; CI and the release
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: foundry-implementation-actor
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Runs a headless Claude Code implementation session against one capability's own repo — a papeete-actor for one use, with the capability supplied by a sidecar.
5
5
  Project-URL: Homepage, https://github.com/papeete-hub/foundry-implementation-actor
6
6
  Author-email: Papeete Consulting <yoann.remy@outlook.com>
@@ -97,6 +97,16 @@ papeete-actor-synchronous-messaging lint-card \
97
97
  "$(python -c 'from foundry_implementation_actor import cards_path; print(cards_path())')"
98
98
  ```
99
99
 
100
+ A use's copy is made by hand, so it can drift: both folders pass `lint-card` independently, and
101
+ neither gate has an opinion about the other. `foundry-implementation-actor lint` therefore runs a
102
+ second check — `conformance.check` — comparing the use's cards against the definition's on the
103
+ **derived wire contract**: the set of doors, and each door's `request_schema`, `completion_schema`
104
+ and `engine`. Those derivations already fold in the data dictionary and the message catalog, so a
105
+ renamed item or a changed reference lands in the payload a caller is validated against.
106
+
107
+ Prose is not compared, on purpose: a use *should* name its real capability and its real peers, and
108
+ `actor.yaml`'s `name:` is its own identity and is required to differ.
109
+
100
110
  Because the cards sit under `src/`, `tests/test_portability.py` greps them too — a capability id or
101
111
  a knowledge tool name written into the actor's own definition fails the build exactly as it would
102
112
  in the code.
@@ -77,6 +77,16 @@ papeete-actor-synchronous-messaging lint-card \
77
77
  "$(python -c 'from foundry_implementation_actor import cards_path; print(cards_path())')"
78
78
  ```
79
79
 
80
+ A use's copy is made by hand, so it can drift: both folders pass `lint-card` independently, and
81
+ neither gate has an opinion about the other. `foundry-implementation-actor lint` therefore runs a
82
+ second check — `conformance.check` — comparing the use's cards against the definition's on the
83
+ **derived wire contract**: the set of doors, and each door's `request_schema`, `completion_schema`
84
+ and `engine`. Those derivations already fold in the data dictionary and the message catalog, so a
85
+ renamed item or a changed reference lands in the payload a caller is validated against.
86
+
87
+ Prose is not compared, on purpose: a use *should* name its real capability and its real peers, and
88
+ `actor.yaml`'s `name:` is its own identity and is required to differ.
89
+
80
90
  Because the cards sit under `src/`, `tests/test_portability.py` greps them too — a capability id or
81
91
  a knowledge tool name written into the actor's own definition fails the build exactly as it would
82
92
  in the code.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "foundry-implementation-actor"
3
- version = "0.2.0"
3
+ version = "0.2.1"
4
4
  description = "Runs a headless Claude Code implementation session against one capability's own repo — a papeete-actor for one use, with the capability supplied by a sidecar."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -22,7 +22,7 @@ from .config import (CapabilityConfig, Component, ConfigError, Grounding, Report
22
22
  cards_path, lint)
23
23
  from .engine import ClaudeCodeEngine
24
24
  from .handler import HandlerError, make_implement_task
25
- from . import correlation, grounding
25
+ from . import conformance, correlation, grounding
26
26
 
27
27
  __all__ = [
28
28
  "CapabilityConfig",
@@ -33,6 +33,7 @@ __all__ = [
33
33
  "HandlerError",
34
34
  "Report",
35
35
  "cards_path",
36
+ "conformance",
36
37
  "correlation",
37
38
  "grounding",
38
39
  "lint",
@@ -13,13 +13,22 @@ import argparse
13
13
  import sys
14
14
  from pathlib import Path
15
15
 
16
+ from . import conformance
16
17
  from .config import CapabilityConfig, ConfigError, lint
17
18
 
18
19
  _REGISTRY_PLACEHOLDER = "<registry>"
19
20
 
20
21
 
21
22
  def _cmd_lint(args: argparse.Namespace) -> int:
23
+ # Two gates, one command. The sidecar says which capability this use serves; the cards say
24
+ # which actor it claims to be. A use can be wrong about either independently, and the second
25
+ # check is the only thing standing between a hand-copied card set and a caller being refused
26
+ # at a door — see `conformance.py`.
22
27
  report = lint(Path(args.folder))
28
+ conformance_report = conformance.check(Path(args.folder))
29
+ report.oks.extend(conformance_report.oks)
30
+ report.warns.extend(conformance_report.warns)
31
+ report.errors.extend(conformance_report.errors)
23
32
  for warning in report.warns:
24
33
  print(f" ! {warning}")
25
34
  for error in report.errors:
@@ -29,7 +38,8 @@ def _cmd_lint(args: argparse.Namespace) -> int:
29
38
  return 1
30
39
  for line in report.oks:
31
40
  print(f" ok {line}")
32
- print("✓ sidecar conforms")
41
+ checked_cards = any((Path(args.folder) / name).exists() for name in conformance.CARD_FILES)
42
+ print("✓ sidecar and cards conform" if checked_cards else "✓ sidecar conforms")
33
43
  return 0
34
44
 
35
45
 
@@ -0,0 +1,119 @@
1
+ """Does this use still answer the same doors as the actor it claims to be?
2
+
3
+ WHY THIS EXISTS. This package ships the actor's DEFINITION — four cards under `cards/` saying what
4
+ a foundry implementation actor is. A USE is one capability's own folder, carrying its own copy of
5
+ those four, named for the capability it serves. The copy is made by hand, because a use is a static
6
+ repository today rather than something spawned from a capability id.
7
+
8
+ A hand copy drifts. Both folders pass `lint-card` independently — each is a conformant actor — and
9
+ neither gate has any opinion about the other. So the day the definition gains a field, renames a
10
+ message, or changes a door's completion set, a use that was not updated keeps linting green and
11
+ starts refusing callers at runtime, with the rejection surfacing at the door rather than here.
12
+
13
+ WHAT IS COMPARED, AND WHAT IS NOT. Only what a caller can observe: the set of doors, and for each
14
+ one its derived `request_schema`, its `completion_schema`, and the `engine` key it resolves
15
+ through. Those derivations already fold in the data dictionary and the message catalog — a renamed
16
+ data item, a changed type, a reference added to a message, all of it lands in the schema a caller is
17
+ validated against — so comparing them separately would be comparing the same fact twice.
18
+
19
+ PROSE IS NOT COMPARED, ON PURPOSE. A use's `means:` should name its real capability and its real
20
+ peer actors; the definition's cannot, because it serves no capability and its cards are grepped for
21
+ exactly such literals. The use's wording is the better one for anyone reading `describe`, and
22
+ flattening it to the definition's would delete information. Nor is `actor.yaml`'s `name:`: that is
23
+ the use's own identity, and it is REQUIRED to differ.
24
+ """
25
+ from __future__ import annotations
26
+
27
+ from pathlib import Path
28
+
29
+ from papeete_actor_synchronous_messaging import card as pas_card
30
+
31
+ from .config import Report, cards_path
32
+
33
+ CARD_FILES = ("actor.yaml", "actor-data.yaml", "actor-message.yaml",
34
+ "actor-synchronous-messaging.yaml")
35
+
36
+
37
+ def check(folder: str | Path = ".") -> Report:
38
+ """Compare one use's cards against the definition this package ships."""
39
+ use = Path(folder)
40
+ report = Report(oks=[], warns=[], errors=[])
41
+
42
+ present = [name for name in CARD_FILES if (use / name).exists()]
43
+ if not present:
44
+ # Not an error. `lint` is also run against a sidecar on its own — in this package's own CI,
45
+ # among other places — and a folder with no cards is not a malformed use, just not a
46
+ # complete one.
47
+ report.warns.append(
48
+ f"{use}: no cards here, so nothing to compare — a use carries its own copy of the "
49
+ f"actor's four cards beside its sidecar"
50
+ )
51
+ return report
52
+ if len(present) != len(CARD_FILES):
53
+ missing = [name for name in CARD_FILES if name not in present]
54
+ report.errors.append(
55
+ f"{use}: an incomplete card set — missing {', '.join(missing)}. `Actor.from_card` "
56
+ f"opens exactly these four and never globs, so a use missing one does not boot."
57
+ )
58
+ return report
59
+
60
+ definition = cards_path()
61
+ if any(not (definition / name).exists() for name in CARD_FILES):
62
+ # A wheel that lost its cards. `lint-card` in CI and in the release workflow exists to stop
63
+ # that reaching PyPI; this turns the leftover case into a report rather than a traceback
64
+ # from inside a gate a consumer is running.
65
+ report.errors.append(
66
+ f"{definition}: this package's own cards are missing, so there is nothing to compare "
67
+ f"against. The build shipped without them — report it against the release."
68
+ )
69
+ return report
70
+
71
+ try:
72
+ theirs = pas_card.load(use)
73
+ except ValueError as e:
74
+ report.errors.append(f"{use}: its own cards do not pass the restriction, so they cannot "
75
+ f"be compared: {e}")
76
+ return report
77
+ ours = pas_card.load(definition)
78
+
79
+ report.errors.extend(_compare(ours, theirs, "actions", use))
80
+ report.errors.extend(_compare(ours, theirs, "queries", use))
81
+ if not report.errors:
82
+ doors = ", ".join(sorted(theirs.actions) + sorted(theirs.queries))
83
+ report.oks.append(f"{use} answers the definition's doors, unchanged: {doors}")
84
+ return report
85
+
86
+
87
+ def _compare(ours, theirs, kind: str, use: Path) -> list[str]:
88
+ """The differences in one door family, as messages. Empty means they agree."""
89
+ defined, used = getattr(ours, kind), getattr(theirs, kind)
90
+ errors = []
91
+
92
+ for extra in sorted(set(used) - set(defined)):
93
+ errors.append(f"{use}: {kind[:-1]} '{extra}' is not a door the definition declares — a use "
94
+ f"answers the actor's doors, it does not add its own")
95
+ for absent in sorted(set(defined) - set(used)):
96
+ errors.append(f"{use}: {kind[:-1]} '{absent}' is missing — the definition declares it, so a "
97
+ f"caller addressing this actor may send it")
98
+
99
+ for door in sorted(set(defined) & set(used)):
100
+ mine, yours = defined[door], used[door]
101
+ if mine.request_schema != yours.request_schema:
102
+ errors.append(
103
+ f"{use}: {kind[:-1]} '{door}' accepts a different payload than the definition. "
104
+ f"Its `door_schema` message and the data items that message references are what "
105
+ f"derive this, so one of the two drifted:\n"
106
+ f" definition {mine.request_schema}\n"
107
+ f" this use {yours.request_schema}")
108
+ if mine.completion_schema != yours.completion_schema:
109
+ errors.append(
110
+ f"{use}: {kind[:-1]} '{door}' replies against a different completion set than the "
111
+ f"definition:\n"
112
+ f" definition {mine.completion_schema}\n"
113
+ f" this use {yours.completion_schema}")
114
+ if mine.engine != yours.engine:
115
+ errors.append(
116
+ f"{use}: {kind[:-1]} '{door}' resolves through engine '{yours.engine}', the "
117
+ f"definition through '{mine.engine}' — the entrypoint registers the engine under "
118
+ f"the sidecar's `engine:` key, so these three must agree")
119
+ return errors
@@ -0,0 +1,38 @@
1
+ # WHAT DATA EXISTS. `papeete-actor-data/v0`, owned by `papeete-actor-message` — a named, minimally
2
+ # typed dictionary. Every field here belongs to the ACTOR, not to any capability: a task id, the
3
+ # caller's statement of the task, and what came back. Nothing names a capability, a component or a
4
+ # knowledge tool, because none of those are known until a sidecar supplies them.
5
+ data: papeete-actor-data/v0
6
+ items:
7
+ - name: task_id
8
+ type: string
9
+ description: the TASK-NNN card id this actor was asked about (e.g. "TASK-009")
10
+ - name: title
11
+ type: string
12
+ description: the task's short title/summary, supplied by the caller — no card lookup is performed here
13
+ - name: context
14
+ type: string
15
+ description: free-text elaboration beyond the title — background, constraints, links; optional
16
+ - name: definition_of_done
17
+ type: list
18
+ description: the acceptance criteria this task must satisfy, as the caller states them
19
+ - name: remediation_context
20
+ type: string
21
+ description: >-
22
+ on a retry, the prior attempt's failing test criteria — what the implementing session should
23
+ fix this time; optional, absent on a first attempt
24
+ - name: accepted
25
+ type: boolean
26
+ description: whether the implement-task request was accepted, pushed, and its images published
27
+ - name: because
28
+ type: string
29
+ description: why a request was refused (containment violation, nothing staged, ...)
30
+ - name: branch
31
+ type: string
32
+ description: the branch the implementation was pushed to (impl/TASK-NNN)
33
+ - name: images
34
+ type: list
35
+ description: >-
36
+ the full name:version ref of each touched component's published image (e.g.
37
+ "acme.parts.cap.sup.007.wid-backend:0.1.0-TASK-011-a1b2c3d"), by convention — no other actor
38
+ is ever told these tags explicitly
@@ -0,0 +1,17 @@
1
+ # WHAT MESSAGES EXIST. `papeete-actor-message/v1`, owned by `papeete-actor-message` — named
2
+ # messages, each a pure regrouping of references into the data dictionary, under an intent.
3
+ # Wire-agnostic by that package's own design: WHAT a message is, never HOW it is carried.
4
+ message: papeete-actor-message/v1
5
+ messages:
6
+ - name: implement-task-cmd
7
+ intent: ask this actor to implement a TASK-NNN card for the capability it serves
8
+ references: [task_id, title, definition_of_done, context, remediation_context]
9
+ optional: [context, remediation_context]
10
+
11
+ - name: task-implemented-result
12
+ intent: report that the task was implemented, pushed, and its touched components' images published
13
+ references: [accepted, branch, images]
14
+
15
+ - name: task-refused-result
16
+ intent: report that the task was not implemented, and why
17
+ references: [accepted, because]
@@ -0,0 +1,21 @@
1
+ # THE DOORS. `synchronous-messaging-doors/v1` — one non-deterministic door, and no queries.
2
+ #
3
+ # `engine: claude-code` is this actor kind's declared engine. A use's sidecar repeats it in its own
4
+ # `engine:` field, and the entrypoint registers the engine under exactly that key — so a use whose
5
+ # sidecar names a different engine is caught by its own card rather than at the first request.
6
+ manifest: synchronous-messaging-doors/v1
7
+ actions:
8
+ - id: implement-task
9
+ means: >-
10
+ the door for "implement TASK-NNN for the capability I serve". Send it here with the task id,
11
+ title, definition of done, and (optionally) context and remediation_context — the caller
12
+ supplies everything, no task card is looked up here. I clone my capability's repository into
13
+ my own private copy, ground myself in that capability's own standing context, judge how to
14
+ implement the task (writing only under whichever of my declared components it touches), then
15
+ commit and push to impl/TASK-NNN, and build each touched component's image in the cluster's
16
+ shared buildkit and push it to the registry, named and versioned by convention. I never open
17
+ a pull request — an orchestrating actor does, once a testing actor also confirms.
18
+ completion: whether I accepted, pushed, and published images (with the branch and image refs), or refused, and why.
19
+ door_schema: implement-task-cmd
20
+ completion_schema: [task-implemented-result, task-refused-result]
21
+ engine: claude-code
@@ -0,0 +1,8 @@
1
+ # A USE's identity card. The other three are copied verbatim from the definition this package
2
+ # ships, which is exactly what `conformance.check` asserts — only this file is a use's own, and
3
+ # only its `name:` and `description:` may differ.
4
+ manifest: papeete-actor-manifest/v0
5
+ name: ACME.PARTS.CAP.SUP.007.WID-implementation
6
+ description: >-
7
+ Implements TASK-NNN cards for the fictional capability ACME.PARTS.CAP.SUP.007.WID. Exists so the
8
+ gates that run against the installed wheel have a complete use to check, not a sidecar alone.
@@ -0,0 +1,111 @@
1
+ """A use answers the actor's doors, or it is not a use of that actor.
2
+
3
+ The definition and a use are two folders, each independently conformant, and neither gate has any
4
+ opinion about the other. `conformance.check` is the one that does — see `conformance.py` for what
5
+ it compares and, just as deliberately, what it does not.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import shutil
10
+
11
+ import pytest
12
+ import yaml
13
+
14
+ from foundry_implementation_actor import cards_path, conformance
15
+
16
+ FIXTURE = "tests/fixtures/valid"
17
+
18
+
19
+ def _use(tmp_path, source=None):
20
+ """A complete use folder, copied so a test can bend one thing in it."""
21
+ folder = tmp_path / "use"
22
+ folder.mkdir()
23
+ for name in conformance.CARD_FILES:
24
+ shutil.copy((source or cards_path()) / name, folder / name)
25
+ return folder
26
+
27
+
28
+ def test_a_verbatim_copy_conforms(tmp_path):
29
+ assert conformance.check(_use(tmp_path)).ok
30
+
31
+
32
+ def test_the_committed_fixture_conforms():
33
+ """The fixture the wheel gates run against is a real use, not a sidecar on its own."""
34
+ report = conformance.check(FIXTURE)
35
+ assert report.ok, report.errors
36
+ assert any("answers the definition's doors" in line for line in report.oks)
37
+
38
+
39
+ def test_no_cards_warns_rather_than_fails(tmp_path):
40
+ """`lint` is also run against a bare sidecar. That is incomplete, not malformed."""
41
+ (tmp_path / "empty").mkdir()
42
+ report = conformance.check(tmp_path / "empty")
43
+ assert report.ok
44
+ assert any("no cards here" in w for w in report.warns)
45
+
46
+
47
+ def test_an_incomplete_card_set_fails(tmp_path):
48
+ folder = _use(tmp_path)
49
+ (folder / "actor-message.yaml").unlink()
50
+ report = conformance.check(folder)
51
+ assert not report.ok
52
+ assert "actor-message.yaml" in report.errors[0]
53
+
54
+
55
+ def test_a_drifted_payload_is_caught(tmp_path):
56
+ """The whole point: the use still lints, and still answers a different door.
57
+
58
+ A reference dropped from the door's own message is the cheapest realistic drift — the card set
59
+ remains internally valid, so every existing gate stays green.
60
+ """
61
+ folder = _use(tmp_path)
62
+ path = folder / "actor-message.yaml"
63
+ doc = yaml.safe_load(path.read_text())
64
+ doc["messages"][0]["references"].remove("title")
65
+ path.write_text(yaml.safe_dump(doc, sort_keys=False))
66
+
67
+ report = conformance.check(folder)
68
+ assert not report.ok
69
+ assert any("accepts a different payload" in e for e in report.errors)
70
+
71
+
72
+ def test_a_dropped_completion_outcome_is_caught(tmp_path):
73
+ folder = _use(tmp_path)
74
+ path = folder / "actor-synchronous-messaging.yaml"
75
+ doc = yaml.safe_load(path.read_text())
76
+ doc["actions"][0]["completion_schema"] = ["task-implemented-result"]
77
+ path.write_text(yaml.safe_dump(doc, sort_keys=False))
78
+
79
+ report = conformance.check(folder)
80
+ assert not report.ok
81
+ assert any("different completion set" in e for e in report.errors)
82
+
83
+
84
+ def test_a_different_engine_is_caught(tmp_path):
85
+ folder = _use(tmp_path)
86
+ path = folder / "actor-synchronous-messaging.yaml"
87
+ doc = yaml.safe_load(path.read_text())
88
+ doc["actions"][0]["engine"] = "some-other-engine"
89
+ path.write_text(yaml.safe_dump(doc, sort_keys=False))
90
+
91
+ report = conformance.check(folder)
92
+ assert not report.ok
93
+ assert any("resolves through engine" in e for e in report.errors)
94
+
95
+
96
+ def test_prose_and_identity_are_not_compared(tmp_path):
97
+ """A use SHOULD name its own capability and its own peers. That is not drift."""
98
+ folder = _use(tmp_path)
99
+
100
+ identity = folder / "actor.yaml"
101
+ doc = yaml.safe_load(identity.read_text())
102
+ doc["name"] = "ACME.PARTS.CAP.SUP.007.WID-implementation"
103
+ doc["description"] = "Implements TASK-NNN cards for ACME.PARTS.CAP.SUP.007.WID."
104
+ identity.write_text(yaml.safe_dump(doc, sort_keys=False))
105
+
106
+ doors = folder / "actor-synchronous-messaging.yaml"
107
+ doc = yaml.safe_load(doors.read_text())
108
+ doc["actions"][0]["means"] = "the door for 'implement TASK-NNN for ACME.PARTS.CAP.SUP.007.WID'."
109
+ doors.write_text(yaml.safe_dump(doc, sort_keys=False))
110
+
111
+ assert conformance.check(folder).ok
@@ -13,7 +13,7 @@ wheels = [
13
13
 
14
14
  [[package]]
15
15
  name = "foundry-implementation-actor"
16
- version = "0.2.0"
16
+ version = "0.2.1"
17
17
  source = { editable = "." }
18
18
  dependencies = [
19
19
  { name = "papeete-actor-synchronous-messaging" },