foundry-implementation-actor 0.1.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.
- {foundry_implementation_actor-0.1.0 → foundry_implementation_actor-0.2.1}/.github/workflows/ci.yml +10 -0
- {foundry_implementation_actor-0.1.0 → foundry_implementation_actor-0.2.1}/.github/workflows/release.yml +9 -0
- {foundry_implementation_actor-0.1.0 → foundry_implementation_actor-0.2.1}/CLAUDE.md +21 -1
- {foundry_implementation_actor-0.1.0 → foundry_implementation_actor-0.2.1}/PKG-INFO +40 -3
- {foundry_implementation_actor-0.1.0 → foundry_implementation_actor-0.2.1}/README.md +39 -2
- {foundry_implementation_actor-0.1.0 → foundry_implementation_actor-0.2.1}/pyproject.toml +1 -1
- {foundry_implementation_actor-0.1.0 → foundry_implementation_actor-0.2.1}/src/foundry_implementation_actor/__init__.py +5 -2
- foundry_implementation_actor-0.2.1/src/foundry_implementation_actor/cards/actor-data.yaml +38 -0
- foundry_implementation_actor-0.2.1/src/foundry_implementation_actor/cards/actor-message.yaml +17 -0
- foundry_implementation_actor-0.2.1/src/foundry_implementation_actor/cards/actor-synchronous-messaging.yaml +21 -0
- foundry_implementation_actor-0.2.1/src/foundry_implementation_actor/cards/actor.yaml +19 -0
- {foundry_implementation_actor-0.1.0 → foundry_implementation_actor-0.2.1}/src/foundry_implementation_actor/cli.py +11 -1
- {foundry_implementation_actor-0.1.0 → foundry_implementation_actor-0.2.1}/src/foundry_implementation_actor/config.py +16 -0
- foundry_implementation_actor-0.2.1/src/foundry_implementation_actor/conformance.py +119 -0
- foundry_implementation_actor-0.2.1/tests/fixtures/valid/actor-data.yaml +38 -0
- foundry_implementation_actor-0.2.1/tests/fixtures/valid/actor-message.yaml +17 -0
- foundry_implementation_actor-0.2.1/tests/fixtures/valid/actor-synchronous-messaging.yaml +21 -0
- foundry_implementation_actor-0.2.1/tests/fixtures/valid/actor.yaml +8 -0
- foundry_implementation_actor-0.2.1/tests/test_cards.py +49 -0
- foundry_implementation_actor-0.2.1/tests/test_conformance.py +111 -0
- {foundry_implementation_actor-0.1.0 → foundry_implementation_actor-0.2.1}/uv.lock +1 -1
- {foundry_implementation_actor-0.1.0 → foundry_implementation_actor-0.2.1}/.gitignore +0 -0
- {foundry_implementation_actor-0.1.0 → foundry_implementation_actor-0.2.1}/adr/ADR-FIA-0001-the-machinery-leaves-the-capability.md +0 -0
- {foundry_implementation_actor-0.1.0 → foundry_implementation_actor-0.2.1}/adr/README.md +0 -0
- {foundry_implementation_actor-0.1.0 → foundry_implementation_actor-0.2.1}/adr/template.md +0 -0
- {foundry_implementation_actor-0.1.0 → foundry_implementation_actor-0.2.1}/scripts/probe_grounding.py +0 -0
- {foundry_implementation_actor-0.1.0 → foundry_implementation_actor-0.2.1}/src/foundry_implementation_actor/correlation.py +0 -0
- {foundry_implementation_actor-0.1.0 → foundry_implementation_actor-0.2.1}/src/foundry_implementation_actor/engine.py +0 -0
- {foundry_implementation_actor-0.1.0 → foundry_implementation_actor-0.2.1}/src/foundry_implementation_actor/grounding.py +0 -0
- {foundry_implementation_actor-0.1.0 → foundry_implementation_actor-0.2.1}/src/foundry_implementation_actor/handler.py +0 -0
- {foundry_implementation_actor-0.1.0 → foundry_implementation_actor-0.2.1}/src/foundry_implementation_actor/schemas/agentic-context.schema.yaml +0 -0
- {foundry_implementation_actor-0.1.0 → foundry_implementation_actor-0.2.1}/tests/conftest.py +0 -0
- {foundry_implementation_actor-0.1.0 → foundry_implementation_actor-0.2.1}/tests/fixtures/broken/actor-agentic-context.yaml +0 -0
- {foundry_implementation_actor-0.1.0 → foundry_implementation_actor-0.2.1}/tests/fixtures/valid/actor-agentic-context.yaml +0 -0
- {foundry_implementation_actor-0.1.0 → foundry_implementation_actor-0.2.1}/tests/test_cli.py +0 -0
- {foundry_implementation_actor-0.1.0 → foundry_implementation_actor-0.2.1}/tests/test_config.py +0 -0
- {foundry_implementation_actor-0.1.0 → foundry_implementation_actor-0.2.1}/tests/test_engine.py +0 -0
- {foundry_implementation_actor-0.1.0 → foundry_implementation_actor-0.2.1}/tests/test_grounding.py +0 -0
- {foundry_implementation_actor-0.1.0 → foundry_implementation_actor-0.2.1}/tests/test_handler.py +0 -0
- {foundry_implementation_actor-0.1.0 → foundry_implementation_actor-0.2.1}/tests/test_portability.py +0 -0
{foundry_implementation_actor-0.1.0 → foundry_implementation_actor-0.2.1}/.github/workflows/ci.yml
RENAMED
|
@@ -32,6 +32,16 @@ jobs:
|
|
|
32
32
|
--registry reg.example.com | tee /tmp/show.txt
|
|
33
33
|
grep -q 'reg.example.com/acme.parts/sup.007.wid/backend:<version>' /tmp/show.txt
|
|
34
34
|
|
|
35
|
+
- name: the wheel must carry the actor's own cards
|
|
36
|
+
# The `-actor` suffix asserts a papeete-actor underneath, which lint-card can check
|
|
37
|
+
# (ADR-ECO-0022). The suite proves the cards are conformant in a source checkout; this
|
|
38
|
+
# proves they SHIPPED. A card folder left out of the build passes every test in tests/ and
|
|
39
|
+
# makes the name a claim the artifact cannot honour. `papeete-actor-synchronous-messaging`
|
|
40
|
+
# is already a runtime dependency, so its gate is in the probe venv with nothing to add.
|
|
41
|
+
run: |
|
|
42
|
+
/tmp/probe/bin/papeete-actor-synchronous-messaging lint-card \
|
|
43
|
+
"$(/tmp/probe/bin/python -c 'from foundry_implementation_actor import cards_path; print(cards_path())')"
|
|
44
|
+
|
|
35
45
|
- name: the gate must run
|
|
36
46
|
# A gate that cannot fail is not a gate. Lint a deliberately non-conformant sidecar with
|
|
37
47
|
# the INSTALLED wheel and require a non-zero exit.
|
|
@@ -29,6 +29,15 @@ jobs:
|
|
|
29
29
|
uv pip install --python /tmp/probe/bin/python -q dist/*.whl
|
|
30
30
|
/tmp/probe/bin/foundry-implementation-actor lint tests/fixtures/valid
|
|
31
31
|
|
|
32
|
+
- name: the wheel must carry the actor's own cards
|
|
33
|
+
# Same reason as the line above, for the other half of what this package ships. The
|
|
34
|
+
# `-actor` suffix asserts a papeete-actor underneath (ADR-ECO-0022); a wheel published
|
|
35
|
+
# without its cards makes the name a claim the artifact cannot honour, and a published
|
|
36
|
+
# version is not something to discover that from.
|
|
37
|
+
run: |
|
|
38
|
+
/tmp/probe/bin/papeete-actor-synchronous-messaging lint-card \
|
|
39
|
+
"$(/tmp/probe/bin/python -c 'from foundry_implementation_actor import cards_path; print(cards_path())')"
|
|
40
|
+
|
|
32
41
|
- name: grounding renders a CLAUDE.md whose imports resolve
|
|
33
42
|
# BEFORE the upload, not after. A wheel that ships without its schema, or whose grounding
|
|
34
43
|
# emits an @-import to a file it never wrote, produces sessions grounded in nothing that
|
|
@@ -29,7 +29,7 @@ There is no separate lint/format command configured in this repo.
|
|
|
29
29
|
|
|
30
30
|
## Architecture
|
|
31
31
|
|
|
32
|
-
|
|
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,10 +44,30 @@ 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
|
|
|
53
|
+
Beside them, two folders of committed contract, both shipped in the wheel:
|
|
54
|
+
|
|
55
|
+
- **`schemas/agentic-context.schema.yaml`** — the sidecar contract this package owns, which
|
|
56
|
+
`lint` checks a use against.
|
|
57
|
+
- **`cards/`** — the actor's own four cards: what this actor IS, as opposed to which capability a
|
|
58
|
+
use of it serves. `cards_path()` returns the folder. They name no capability, and they are what
|
|
59
|
+
a spawned instance would be rendered from once a use is not a static repository.
|
|
60
|
+
|
|
49
61
|
## Core invariants that any change must preserve
|
|
50
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.
|
|
67
|
+
- **The `-actor` suffix is a claim, and it is checked.** `ADR-ECO-0022`: a package ending in
|
|
68
|
+
`-actor` asserts a `papeete-actor` underneath, and one that ships no conformant card is
|
|
69
|
+
misnamed. `tests/test_cards.py` runs `lint-card` on `cards/` in the suite; CI and the release
|
|
70
|
+
workflow run it again against the built wheel, so the cards cannot silently stop shipping.
|
|
51
71
|
- **No capability literal, ever.** `tests/test_portability.py` greps `src/` for the originating
|
|
52
72
|
instance's identifiers *and* for any knowledge tool name (`kpack`, `kontract`, …), and fails on
|
|
53
73
|
either. The tools a capability grounds itself in are the consumer's dependencies. A fourth
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: foundry-implementation-actor
|
|
3
|
-
Version: 0.1
|
|
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>
|
|
@@ -34,8 +34,8 @@ pip install foundry-implementation-actor
|
|
|
34
34
|
|
|
35
35
|
## What it is
|
|
36
36
|
|
|
37
|
-
The
|
|
38
|
-
sidecar the consuming repo writes:
|
|
37
|
+
The actor's **definition** — its four cards, and the machinery behind them. It carries **no
|
|
38
|
+
capability of its own**: the capability it serves arrives in a sidecar the consuming repo writes:
|
|
39
39
|
|
|
40
40
|
```yaml
|
|
41
41
|
# actor-agentic-context.yaml
|
|
@@ -74,6 +74,43 @@ actor = Actor.from_card(".", mailbox=mailbox,
|
|
|
74
74
|
A second capability instantiates the same actor by writing that file. Nothing here is subclassed,
|
|
75
75
|
hooked, or configured with a strategy object — there is one shape, and it is this one.
|
|
76
76
|
|
|
77
|
+
## The definition, and a use
|
|
78
|
+
|
|
79
|
+
This package is where the actor is **defined**. `src/foundry_implementation_actor/cards/` holds its
|
|
80
|
+
four cards — who it is, the data it knows, the messages it exchanges, and the one `implement-task`
|
|
81
|
+
door it answers — and they ship in the wheel, reachable as `cards_path()`. They name no capability,
|
|
82
|
+
because which capability an instance serves is not part of what the actor *is*.
|
|
83
|
+
|
|
84
|
+
A **use** of this actor is one capability's own folder: its own copy of those four cards, named for
|
|
85
|
+
the capability it serves, beside the `actor-agentic-context.yaml` that binds it to that capability's
|
|
86
|
+
repository and knowledge base. Today that folder is a static repository, and the copy is made by
|
|
87
|
+
hand. Once an instance can be spawned from a capability id alone, the cards here are what it would
|
|
88
|
+
be rendered from — which is why they live in the wheel rather than in an `examples/` folder.
|
|
89
|
+
|
|
90
|
+
The split is what the name asserts. `ADR-ECO-0022` makes the `-actor` suffix an obligation: a
|
|
91
|
+
package ending in `-actor` claims a `papeete-actor` underneath, *"and a `<use>-<tier>-actor` that
|
|
92
|
+
ships no conformant card is misnamed, not merely unusual."* `tests/test_cards.py` runs that check
|
|
93
|
+
in the suite, and CI runs it again against the built wheel:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
papeete-actor-synchronous-messaging lint-card \
|
|
97
|
+
"$(python -c 'from foundry_implementation_actor import cards_path; print(cards_path())')"
|
|
98
|
+
```
|
|
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
|
+
|
|
110
|
+
Because the cards sit under `src/`, `tests/test_portability.py` greps them too — a capability id or
|
|
111
|
+
a knowledge tool name written into the actor's own definition fails the build exactly as it would
|
|
112
|
+
in the code.
|
|
113
|
+
|
|
77
114
|
## What one request does
|
|
78
115
|
|
|
79
116
|
```
|
|
@@ -14,8 +14,8 @@ pip install foundry-implementation-actor
|
|
|
14
14
|
|
|
15
15
|
## What it is
|
|
16
16
|
|
|
17
|
-
The
|
|
18
|
-
sidecar the consuming repo writes:
|
|
17
|
+
The actor's **definition** — its four cards, and the machinery behind them. It carries **no
|
|
18
|
+
capability of its own**: the capability it serves arrives in a sidecar the consuming repo writes:
|
|
19
19
|
|
|
20
20
|
```yaml
|
|
21
21
|
# actor-agentic-context.yaml
|
|
@@ -54,6 +54,43 @@ actor = Actor.from_card(".", mailbox=mailbox,
|
|
|
54
54
|
A second capability instantiates the same actor by writing that file. Nothing here is subclassed,
|
|
55
55
|
hooked, or configured with a strategy object — there is one shape, and it is this one.
|
|
56
56
|
|
|
57
|
+
## The definition, and a use
|
|
58
|
+
|
|
59
|
+
This package is where the actor is **defined**. `src/foundry_implementation_actor/cards/` holds its
|
|
60
|
+
four cards — who it is, the data it knows, the messages it exchanges, and the one `implement-task`
|
|
61
|
+
door it answers — and they ship in the wheel, reachable as `cards_path()`. They name no capability,
|
|
62
|
+
because which capability an instance serves is not part of what the actor *is*.
|
|
63
|
+
|
|
64
|
+
A **use** of this actor is one capability's own folder: its own copy of those four cards, named for
|
|
65
|
+
the capability it serves, beside the `actor-agentic-context.yaml` that binds it to that capability's
|
|
66
|
+
repository and knowledge base. Today that folder is a static repository, and the copy is made by
|
|
67
|
+
hand. Once an instance can be spawned from a capability id alone, the cards here are what it would
|
|
68
|
+
be rendered from — which is why they live in the wheel rather than in an `examples/` folder.
|
|
69
|
+
|
|
70
|
+
The split is what the name asserts. `ADR-ECO-0022` makes the `-actor` suffix an obligation: a
|
|
71
|
+
package ending in `-actor` claims a `papeete-actor` underneath, *"and a `<use>-<tier>-actor` that
|
|
72
|
+
ships no conformant card is misnamed, not merely unusual."* `tests/test_cards.py` runs that check
|
|
73
|
+
in the suite, and CI runs it again against the built wheel:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
papeete-actor-synchronous-messaging lint-card \
|
|
77
|
+
"$(python -c 'from foundry_implementation_actor import cards_path; print(cards_path())')"
|
|
78
|
+
```
|
|
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
|
+
|
|
90
|
+
Because the cards sit under `src/`, `tests/test_portability.py` greps them too — a capability id or
|
|
91
|
+
a knowledge tool name written into the actor's own definition fails the build exactly as it would
|
|
92
|
+
in the code.
|
|
93
|
+
|
|
57
94
|
## What one request does
|
|
58
95
|
|
|
59
96
|
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "foundry-implementation-actor"
|
|
3
|
-
version = "0.1
|
|
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"
|
|
@@ -18,10 +18,11 @@ Wiring one up is four lines:
|
|
|
18
18
|
`correlation` is exported too — an entrypoint installs its filter on the root logger's handlers
|
|
19
19
|
after configuring observability, so every record the process emits carries this request's ids.
|
|
20
20
|
"""
|
|
21
|
-
from .config import CapabilityConfig, Component, ConfigError, Grounding, Report,
|
|
21
|
+
from .config import (CapabilityConfig, Component, ConfigError, Grounding, Report,
|
|
22
|
+
cards_path, lint)
|
|
22
23
|
from .engine import ClaudeCodeEngine
|
|
23
24
|
from .handler import HandlerError, make_implement_task
|
|
24
|
-
from . import correlation, grounding
|
|
25
|
+
from . import conformance, correlation, grounding
|
|
25
26
|
|
|
26
27
|
__all__ = [
|
|
27
28
|
"CapabilityConfig",
|
|
@@ -31,6 +32,8 @@ __all__ = [
|
|
|
31
32
|
"Grounding",
|
|
32
33
|
"HandlerError",
|
|
33
34
|
"Report",
|
|
35
|
+
"cards_path",
|
|
36
|
+
"conformance",
|
|
34
37
|
"correlation",
|
|
35
38
|
"grounding",
|
|
36
39
|
"lint",
|
|
@@ -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,19 @@
|
|
|
1
|
+
# WHO. The actor this package defines — `papeete-actor-manifest/v0`, owned by `papeete-actor`.
|
|
2
|
+
#
|
|
3
|
+
# THIS FOLDER IS THE DEFINITION, NOT A USE. The four cards here say what a foundry implementation
|
|
4
|
+
# actor IS: the data it knows, the messages it exchanges, and the one door it answers. They name no
|
|
5
|
+
# capability, because which capability an instance serves is not part of what the actor is — that
|
|
6
|
+
# is `actor-agentic-context.yaml`, supplied per use.
|
|
7
|
+
#
|
|
8
|
+
# One use of this actor is a folder carrying its own copy of these four cards, named for the
|
|
9
|
+
# capability it serves, beside the sidecar that binds it to that capability's knowledge base. Today
|
|
10
|
+
# that folder is a static repository; the cards here are what a spawned instance would be rendered
|
|
11
|
+
# from once it is not.
|
|
12
|
+
manifest: papeete-actor-manifest/v0
|
|
13
|
+
name: foundry-implementation-actor
|
|
14
|
+
description: >-
|
|
15
|
+
Implements one TASK-NNN card for the business capability its sidecar names. Clones that
|
|
16
|
+
capability's own repository into a private copy, grounds the session in the capability's own
|
|
17
|
+
standing context, and writes only under the components the sidecar declares — then commits,
|
|
18
|
+
pushes a branch, and publishes one image per component the task actually touched. Never opens a
|
|
19
|
+
pull request: an orchestrating actor does that, once a testing actor also confirms.
|
|
@@ -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
|
-
|
|
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
|
|
|
@@ -45,6 +45,7 @@ CONTRACT = "foundry-implementation-actor/agentic-context/v1"
|
|
|
45
45
|
SIDECAR = "actor-agentic-context.yaml"
|
|
46
46
|
|
|
47
47
|
_SCHEMA_PATH = Path(__file__).resolve().parent / "schemas" / "agentic-context.schema.yaml"
|
|
48
|
+
_CARDS_PATH = Path(__file__).resolve().parent / "cards"
|
|
48
49
|
|
|
49
50
|
# The segment a capability id carries to say "capability". It is dropped from the registry path
|
|
50
51
|
# because the path position already says it — every other token of the id survives, across
|
|
@@ -56,6 +57,21 @@ _CAPABILITY_SEGMENT = "cap"
|
|
|
56
57
|
_PLACEHOLDER = re.compile(r"\{([a-z_]+)\}")
|
|
57
58
|
|
|
58
59
|
|
|
60
|
+
def cards_path() -> Path:
|
|
61
|
+
"""The folder holding this actor's own four cards — its definition, shipped in the wheel.
|
|
62
|
+
|
|
63
|
+
The cards say what a foundry implementation actor IS: its data dictionary, its message
|
|
64
|
+
catalog, and the one door it answers. They name no capability, because which capability an
|
|
65
|
+
instance serves is not part of what the actor is — that is the sidecar, supplied per use.
|
|
66
|
+
|
|
67
|
+
A use is today a static folder carrying its own copy of these four, named for the capability
|
|
68
|
+
it serves. This path is what a spawned instance would be rendered from once it is not, and it
|
|
69
|
+
is what `papeete-actor-synchronous-messaging lint-card` is pointed at to check that the
|
|
70
|
+
`-actor` suffix in this package's name is a claim it actually honours (ADR-ECO-0022).
|
|
71
|
+
"""
|
|
72
|
+
return _CARDS_PATH
|
|
73
|
+
|
|
74
|
+
|
|
59
75
|
def load_schema() -> dict:
|
|
60
76
|
"""The contract, as committed source inside this package.
|
|
61
77
|
|
|
@@ -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,49 @@
|
|
|
1
|
+
"""The `-actor` suffix, checked rather than asserted.
|
|
2
|
+
|
|
3
|
+
`ADR-ECO-0022` makes the suffix an obligation: "A package ending in `-actor` asserts a
|
|
4
|
+
`papeete-actor` underneath, which lint-card can check. A future `<use>-<tier>-actor` that ships no
|
|
5
|
+
conformant card is misnamed, not merely unusual."
|
|
6
|
+
|
|
7
|
+
This file is that check. It runs the same gate `papeete-actor-synchronous-messaging lint-card` runs,
|
|
8
|
+
against the cards this package ships — so the claim in the name is a test, not a sentence in a
|
|
9
|
+
record.
|
|
10
|
+
|
|
11
|
+
The cards live under `src/`, which means `test_portability.py` greps them too: a capability id or a
|
|
12
|
+
knowledge tool name written into the actor's own definition fails the build exactly as it would in
|
|
13
|
+
the code.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from papeete_actor_synchronous_messaging import card
|
|
18
|
+
|
|
19
|
+
from foundry_implementation_actor import cards_path
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_the_package_ships_the_four_cards():
|
|
23
|
+
"""A card folder is four files; `Actor.from_card` opens exactly these and never globs."""
|
|
24
|
+
folder = cards_path()
|
|
25
|
+
missing = [name for name in ("actor.yaml", "actor-data.yaml", "actor-message.yaml",
|
|
26
|
+
"actor-synchronous-messaging.yaml")
|
|
27
|
+
if not (folder / name).exists()]
|
|
28
|
+
assert not missing, f"the actor's definition is incomplete — missing: {', '.join(missing)}"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_the_shipped_cards_are_conformant():
|
|
32
|
+
"""The whole of the `-actor` claim: this folder is a papeete-actor, or the name is wrong."""
|
|
33
|
+
report = card.lint(cards_path())
|
|
34
|
+
assert not report.errors, ("the cards this package ships are not conformant:\n "
|
|
35
|
+
+ "\n ".join(report.errors))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_the_door_names_the_engine_a_sidecar_must_declare():
|
|
39
|
+
"""The card's door and a use's sidecar name the same engine key.
|
|
40
|
+
|
|
41
|
+
The entrypoint registers the engine under the sidecar's `engine:`, and the door resolves
|
|
42
|
+
through the card's. They are two statements of one fact, in two repos; this pins the half that
|
|
43
|
+
lives here so a use's `lint` failure is the only way they can disagree.
|
|
44
|
+
"""
|
|
45
|
+
loaded = card.load(cards_path())
|
|
46
|
+
assert set(loaded.actions) == {"implement-task"}, (
|
|
47
|
+
f"expected the one implement-task door, got {sorted(loaded.actions)}")
|
|
48
|
+
engines = {offer.engine for offer in loaded.actions.values()}
|
|
49
|
+
assert engines == {"claude-code"}, f"expected the one claude-code door, got {engines}"
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{foundry_implementation_actor-0.1.0 → foundry_implementation_actor-0.2.1}/scripts/probe_grounding.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{foundry_implementation_actor-0.1.0 → foundry_implementation_actor-0.2.1}/tests/test_config.py
RENAMED
|
File without changes
|
{foundry_implementation_actor-0.1.0 → foundry_implementation_actor-0.2.1}/tests/test_engine.py
RENAMED
|
File without changes
|
{foundry_implementation_actor-0.1.0 → foundry_implementation_actor-0.2.1}/tests/test_grounding.py
RENAMED
|
File without changes
|
{foundry_implementation_actor-0.1.0 → foundry_implementation_actor-0.2.1}/tests/test_handler.py
RENAMED
|
File without changes
|
{foundry_implementation_actor-0.1.0 → foundry_implementation_actor-0.2.1}/tests/test_portability.py
RENAMED
|
File without changes
|