Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 94 additions & 17 deletions .github/agents/editor-lens.agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,26 @@ target is a codebase where every remaining comment is one a maintainer
would write today, from scratch, knowing nothing about the PR that
introduced it.

A second, broader mandate: catch **cryptic references to internal
review artifacts** wherever they appear in the diff, including
user-facing files (`README.md`, `sphinx/source/**`, `CHANGELOG.md`,
top-level policy docs, `.github/**`). Finding IDs (`F3`, `G5`,
`H2`), remediation slugs, round/chunk markers, and back-references
to internal sketches / plans / review files are useful while a PR
is in flight but have no meaning to a downstream reader. Past PRs
have leaked these into published docs; the cryptic-reference sweep
is the backstop that catches them at finalize.

## Scope

**In scope** — code prose only:
The lens has **two scopes**: a broad *cryptic-reference sweep* that
applies everywhere there is text, and a narrower *full prose edit*
scope where it may also apply the keep / rewrite / cut policy.

### Full prose edit — in scope

Apply the full Keep / Rewrite / Cut policy below to **code prose
only**:

- `src/bocpy/**/*.{c,h,py,pyi}` (the library, including `_core.c`,
`_math.c`, `boc_*.{c,h}`, `behaviors.py`, `transpiler.py`, `worker.py`,
Expand All @@ -37,20 +54,67 @@ introduced it.
- `templates/c_abi_consumer/src/**/*.{c,h,py}`
- `scripts/**/*.py`

**Out of scope — do not touch:**
### Full prose edit — out of scope, do not touch

- `sphinx/source/**` — narrative documentation; different rules,
managed by the docs step of `finalize-pr`.
- `README.md` — user-facing entry point; outside this lens's mandate.
- `CHANGELOG.md` — append-only history; managed by the changelog step
of `finalize-pr`.
These have different rules (Sphinx narrative, user-facing entry
point, append-only history, policy docs, meta config). Do not apply
the general Keep / Rewrite / Cut policy here:

- `sphinx/source/**` — narrative documentation; managed by the
docs step of `finalize-pr`.
- `README.md` — user-facing entry point.
- `CHANGELOG.md` — append-only history; managed by the changelog
step of `finalize-pr`.
- `CONTRIBUTING.md`, `SUPPLY_CHAIN.md`, `SUPPORT.md`, `SECURITY.md`,
`CODE_OF_CONDUCT.md` — top-level policy docs.
- `.github/**` — agent / skill / workflow definitions (meta).
- `.copilot/**` — scratch.
- `templates/c_abi_consumer/{README.md,pyproject.toml}` — template
docs read by downstream consumers.

### Cryptic-reference sweep — applies everywhere

In **every text file** in the branch diff — including the
full-prose-out-of-scope set above, *except* `.copilot/**` — also
scan for and flag **cryptic references to internal review
artifacts** that leaked out of in-flight PR machinery:

- Finding IDs and remediation slugs: `F1`, `G3`, `H2`, `M5`, `L2`,
`H1–H4`, "Remediation B6", "per F2", "closes G5".
- Round / iteration / chunk markers: "Round-2 adv#6", "iter-3",
"adversarial-iter1", "chunk 4", "step 7e".
- Back-references to internal review or plan files that ship in
the public docs: "see review-finding-1.md",
"per .copilot/plans/X/40-draft-plan.md",
"sketch ID 23", "see PR-Plan Tier 4 item 13".
- Internal codename references that have no public meaning:
"main-pinned-cowns branch", "the X1 refactor".

For these, the rule is uniform regardless of which file the
reference appears in:

- If the reference is the *whole* point of the line / paragraph,
cut it.
- If the surrounding prose stands on its own once the reference is
removed, rewrite to drop the reference and keep the prose.
- If removing it would damage the surrounding prose, flag under
"Questions for the user" with a proposed rewrite — do **not**
silently rewrite user-facing docs (README, Sphinx, policy files).

The sweep is constrained to the *cryptic-reference* category only.
When operating on out-of-scope files you may **only** remove
cryptic references; you may **not** otherwise trim wordiness,
collapse paragraphs, or restructure the prose. The rest of the
Keep / Rewrite / Cut policy below does not apply to those files.

Rationale: PR-process tags (F#, G#, H#, remediation IDs, sketch
backrefs) are useful while the PR is in flight, but they have no
meaning to a user reading the published README, the Sphinx site,
or the changelog months later. Past PRs have shipped
`per F3 finding` into the README and `closes G5` into the
changelog; this sweep is the backstop that catches them at
finalize.

## Keep / Rewrite / Cut Policy

### Keep (do not touch)
Expand Down Expand Up @@ -173,16 +237,27 @@ introduced it.

When reviewing, produce findings in these sections:

1. **Cuts (high confidence)** — comments that are pure scaffolding,
1. **Cryptic-reference cuts (all scopes)** — PR slugs, finding IDs,
remediation tags, round/chunk markers, and internal sketch /
plan / review backrefs leaked into any text file in the diff.
Group by file. Cuts inside the *full prose edit* scope can be
deleted; cuts inside user-facing docs (`README.md`,
`sphinx/source/**`, `CHANGELOG.md`, top-level policy files,
`.github/**`) must list the proposed rewrite verbatim so the
user can approve before the change lands.
2. **Cuts (high confidence)** — comments that are pure scaffolding,
archaeology, or paraphrase. List file + line range + the comment
text. These can be deleted without further review.
2. **Rewrites** — wordy or stale comments that should be collapsed.
For each, give the original and the proposed replacement.
3. **Keep with edit** — load-bearing comments that need a small fix
(stale file path, wrong PEP number, dated phrasing).
4. **Keep as-is** — comments that initially looked like candidates
text. These can be deleted without further review. *Full prose
edit scope only.*
3. **Rewrites** — wordy or stale comments that should be collapsed.
For each, give the original and the proposed replacement. *Full
prose edit scope only.*
4. **Keep with edit** — load-bearing comments that need a small fix
(stale file path, wrong PEP number, dated phrasing). *Full prose
edit scope only.*
5. **Keep as-is** — comments that initially looked like candidates
but are actually load-bearing. Brief justification each.
5. **Questions for the user** — comments whose intent is unclear and
6. **Questions for the user** — comments whose intent is unclear and
that should not be removed without confirmation. Include the
comment and what's ambiguous. Always include any `TODO` / `FIXME`
without an issue or sketch link.
Expand All @@ -197,9 +272,11 @@ findings remain.

- **Adding new comments.** This lens removes; it does not author.
The usability lens authors.
- **Editing prose under `sphinx/source/`, `README.md`,
- **Rewriting prose in `sphinx/source/`, `README.md`,
`CHANGELOG.md`, the top-level policy docs, or anything under
`.github/`.**
`.github/` beyond removing cryptic internal references.** The
cryptic-reference sweep is the *only* edit permitted in those
files; general wordiness / archaeology / banner cuts are not.
- **Rewriting code.** Behavior is out of scope.
- **Style enforcement** (formatting, capitalization, period-at-end)
unless it is a side effect of an otherwise-justified rewrite.
11 changes: 8 additions & 3 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,9 +190,14 @@ interpreter's headers.
extracts each decorated function into a top-level `__behavior__N` definition
and rewrites the call site as `whencall('__behavior__N', cowns, captures)`.
The captures tuple is built **at schedule time**, so loop variables are
snapshotted by value — no `x=x` default-arg idiom is needed (and adding one
breaks the behavior because the transpiler treats every signature name as a
behavior parameter and discards the default).
snapshotted by value. Two spellings of the loop-snapshot idiom are
supported transparently: just reference the loop variable in the body, or
write `def b(c, i=i)` and let the transpiler hoist the default into a
capture. Trailing positional parameters beyond the cown count are also
auto-captured by name (`def b(c, factor)` captures `factor`). The
transpiler also recognises aliased decorators — `from bocpy import when as
boc_when` and `import bocpy [as alias]` followed by `@bocpy.when(...)` or
`@alias.when(...)` — provided the aliasing import is at module level.

When debugging behavior dispatch, capture resolution, parameter-count
mismatches, or anything else that depends on the transpiler's output, use the
Expand Down
73 changes: 44 additions & 29 deletions .github/skills/testing-with-boc/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,73 +23,88 @@ execute asynchronously on worker interpreters.
| Concept | Description |
|---------|-------------|
| `Cown(value)` | A concurrently-owned wrapper. Behaviors receive exclusive temporal access to the cown's `.value`. |
| `@when(*cowns)` | Decorator that schedules the function as a behavior. The decorator replaces the function with a `Cown` holding the return value. **The decorated function must have exactly as many parameters as there are arguments to `@when`.** |
| `@when(*cowns)` | Decorator that schedules the function as a behavior. The decorator replaces the function with a `Cown` holding the return value. The first N parameters bind to the N cowns; any trailing parameters are auto-captured from the caller's frame (see below). |
| `send(tag, contents)` | Sends a cross-interpreter message with the given tag. |
| `receive(tags, timeout)` | Blocks until a message with a matching tag arrives (or times out). Returns `(TIMEOUT, None)` on timeout. |
| `TIMEOUT` | Sentinel string returned as the tag by `receive` when a timeout elapses. |
| `wait(timeout)` | Blocks until all scheduled behaviors have completed. |

### Critical rule: parameter count must match `@when` argument count
### Cown count, parameter count, and auto-captured extras

The number of parameters on the decorated function **must exactly equal** the
number of arguments passed to `@when`. A mismatch causes unspecified behavior
that can crash the worker interpreter. Because the behavior never completes, the
test will hang forever unless `receive` is called with a timeout.
The first `N` parameters of the decorated function bind positionally to the
`N` arguments of `@when`. Any **additional** trailing parameters are treated
as captures of names from the caller's scope:

* `def b(c, factor)` — captures `factor` by the parameter's own name.
* `def b(c, i=i)` — captures `i` (the canonical loop-snapshot idiom).
* `def b(c, x=y)` — captures `y` and binds it into param `x`.

Defaults must be plain names; computed defaults (`def b(c, k=foo())`) and
defaults on cown positions (`def b(c=c)`) raise `SyntaxError` at export
time. Free variables referenced in the body (but not in the signature) are
also auto-captured, so the simplest spelling is usually to omit the extras
entirely and just reference them in the body.

```python
# CORRECT — 1 @when arg, 1 function param
@when(x)
def good(x):
return x.value * 2

# WRONG — 1 @when arg, 2 function params (default args count!)
# ALSO CORRECT — extra params beyond the cown count are auto-captured
# from the caller's frame by name. Plain extras use the param's own
# name; defaults of the form ``i=i`` or ``x=y`` use the default's name.
factor = 2
@when(x)
def bad(x, factor=2): # will crash — factor is an extra param
def with_extra(x, factor): # ``factor`` captured by name
return x.value * factor

# FIX — capture extra values via closure, not default args
# FIX — for older code: capture extra values via closure
factor = 2
@when(x)
def fixed(x): # 1 param matches 1 @when arg
return x.value * factor # factor captured from enclosing scope
def fixed(x): # 1 param matches 1 @when arg
return x.value * factor # factor captured from enclosing scope
```

### Do not use the `def _(c, x=x)` loop-capture idiom
### The `def _(c, i=i)` loop-capture idiom is supported

A common Python idiom for snapshotting a loop variable is to bind it as a
default argument:
The canonical Python idiom for snapshotting a loop variable as a default
argument works transparently:

```python
for i, c in enumerate(cowns):
@when(c)
def _(c, i=i): # unnecessary AND breaks @when
def _(c, i=i): # ``i`` captured at schedule time
send("done", i)
```

**You don't need this with `@when`.** The transpiler rewrites the call site as
`whencall('__behavior__N', (c,), (i,))`, snapshotting captures into a tuple at
schedule time. There is no late-binding hazard to defend against — just
reference the loop variable directly:
The transpiler hoists positional parameters beyond the cown count into
captures: bare extras (`def b(c, factor)`) capture by the parameter's own
name; defaults (`def b(c, i=i)` or the rename form `def b(c, x=y)`) capture by
the default expression's name. The default expression must be a plain
`Name`; computed defaults (`def b(c, k=foo())`) and defaults on cown
positions (`def b(c=c)`) raise `SyntaxError` at export time.

Because the transpiler already snapshots loop variables into a tuple at
schedule time, you can also just reference the loop variable directly without
the `i=i` idiom — both spellings work:

```python
for i, c in enumerate(cowns):
@when(c)
def _(c):
send("done", i) # i is captured by value at schedule time
send("done", i) # i is captured by value at schedule time
```

Adding `i=i` to the signature actively breaks the behavior. The transpiler
treats every name in the signature as a behavior parameter and discards the
default, so the worker sees a function with an extra positional arg that the
runtime never supplies. See the "Inspecting Transpiler Output" section of
`.github/copilot-instructions.md` for how to use `export_module.py` to
confirm exactly which names are parameters and which are captures.
See the "Inspecting Transpiler Output" section of
`.github/copilot-instructions.md` for how to use `export_module.py` to confirm
exactly which names are parameters and which are captures.

If you do want a fresh scope per iteration (e.g. to avoid sharing mutable
state between iterations), use a helper function:
If you want a fresh scope per iteration (e.g. to avoid sharing mutable state
between iterations), use a helper function:

```python
def _schedule(c, i): # fresh scope per iteration
def _schedule(c, i): # fresh scope per iteration
@when(c)
def _(c):
send("done", i)
Expand Down
14 changes: 7 additions & 7 deletions .github/workflows/build_wheels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
python: [cp310, cp311, cp312, cp313, cp314]

steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Git config for fetching pull requests
run: |
Expand Down Expand Up @@ -61,7 +61,7 @@ jobs:
python: [cp310, cp311, cp312, cp313, cp314]

steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
id: python
Expand Down Expand Up @@ -112,7 +112,7 @@ jobs:
python: [cp310, cp311, cp312, cp313, cp314]

steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
id: python
Expand Down Expand Up @@ -164,7 +164,7 @@ jobs:
python: [cp310, cp311, cp312, cp313, cp314]

steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Git config for fetching pull requests
run: |
Expand Down Expand Up @@ -206,7 +206,7 @@ jobs:
python: [cp310, cp311, cp312, cp313, cp314]

steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Git config for fetching pull requests
run: |
Expand Down Expand Up @@ -269,7 +269,7 @@ jobs:
with:
python-version: "3.14"
- name: Checkout (for scripts/)
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
path: source
- name: Validate every wheel against PyPI's checks
Expand Down Expand Up @@ -336,7 +336,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Use Python 3.14
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/nightly_audit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Use Python 3.14
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
Expand Down
Loading
Loading