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
5 changes: 5 additions & 0 deletions .githooks/pre-push
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/sh
set -eu
cd "$(git rev-parse --show-toplevel)"
command -v node >/dev/null 2>&1 || { echo "pre-push: Node.js is required for the comment gate" >&2; exit 1; }
exec node tools/comment-gate.cjs
13 changes: 13 additions & 0 deletions .github/workflows/comments.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: Source comment policy

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Deliver the workflow through patch/

This commit adds a workflow directly, but the repository requires workflow changes from agent sessions to arrive as a git format-patch plus an applying README under patch/, because the supported credentials cannot push .github/workflows/. Move this addition to that delivery mechanism; otherwise the changeset cannot be delivered through the supported path.

AGENTS.md reference: AGENTS.md:L231-L238

Useful? React with 👍 / 👎.

on: [push, pull_request]
permissions:
contents: read
jobs:
comments:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '24'
- run: make comments comments-test
8 changes: 8 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -340,3 +340,11 @@ Cross-repo sequencing, and what this repo owes station and when, is in
[station-and-plugin-plan.md](https://github.com/voxgig/station/blob/main/docs/design/station-and-plugin-plan.md).
The agreed position between the two designs is
[station-and-plugin.md](https://github.com/voxgig/station/blob/main/docs/design/station-and-plugin.md).

## Source code comments

Follow [COMMENT-POLICY.md](COMMENT-POLICY.md): comments are sparse and terse,
only for intricate or surprising code. Names carry intent; documents carry
requirements. Run `make comments comments-test` after editing source.

Durable implementation rationale is in [COMMENT-NOTES.md](COMMENT-NOTES.md).
11 changes: 11 additions & 0 deletions COMMENT-NOTES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Implementation rationale

Host transitions are sequential and reconciliation reaches a fixed point eagerly. Reentrant lifecycle transitions are rejected. These properties allow the same lifecycle semantics in runtimes with different concurrency mechanisms.

Capability providers are ranked by descending version, ascending priority, then declaration position. Point-binding order bands do not rank capabilities: a provider may have several bindings or no bindings.

Rust drops RefCell borrows before invoking callbacks. Holding a borrow over plugin code can turn a lifecycle operation into a panic.

Sources: [host](typescript/src/Host.ts), [capability resolution](go/plugin/capability.go), [Rust guide](rust/AGENTS.md).

Corpus shape validation is separate from JSON generation so optional empty containers retain their assertion meaning. The runner checks cross-field relationships and nonempty test groups. Artifact validation checks that the corpus version marker exists; unification alone can fill an absent marker.
51 changes: 51 additions & 0 deletions COMMENT-POLICY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Source code comment policy

Adapted from [Aontu ADR-032](https://github.com/aontu-lang/aontu/blob/main/ADR.md#adr-032--code-comments-are-sparse-and-terse-intent-lives-in-names-requirements-live-in-documents).

Comments are sparse and terse. Use them only for intricate or surprising code:
why an unobvious form is necessary, or what a maintainer could otherwise break.
Prefer a precise identifier, named constant, or extracted function to an
explanation of what the code does. Business rules and requirements belong in
documents. History belongs in commit messages and issues.

A line or two is the norm. Delete redundant or stale comments; move durable
explanations into a design document. A coverage exclusion still needs its short
justification. Paths, backticked symbols, and ADR numbers must resolve. Avoid
issue numbers, dates, versions, unverifiable counts, TODO narratives, and
commented-out code. These rules apply to authored code in every language.

## Automated enforcement

`make comments` runs `tools/comment-gate.cjs`. `make comments-test` runs its
positive and negative tests, including the repository gate. CI runs both on
pushes and pull requests. `make hooks` installs the tracked pre-push hook;
Node.js and Git are required, and a missing Node.js fails the hook.

The automated scope covers `.ts`, `.go`, `.rs`, `.aon`, and `.aontu`, including
authored models, test corpora and generator templates. Other languages follow
the written policy through review. JavaScript build/release tooling is outside
the automated scope, as in Aontu. Tracked and unignored new files are checked;
symlinks, dependency directories, compiled output and Rust build artifacts are
not. `tools/comment-scope.json` lists any additional exclusions with reasons.
License notices and compiler, lint, formatting and coverage directives remain.
Generator slot and insertion markers (such as `<[SLOT]>`, `#SecretsImport`, and `EJECT-START`/`EJECT-END`)
are executable tooling directives and are exempt too.
Aontu models use `#` line comments; quoted strings and multiline backtick values
are preserved. Authored `model/target/` files are included. Generated model
goldens are excluded explicitly through `excludeModels` in the scope config;
this does not exclude implementation files in the same directories.

| Rule | Rejected content |
| --- | --- |
| `long-block` | More than 5 consecutive comment lines |
| `dense-file` | More than 12% comment/code lines, when comment lines exceed 8 |
| `narrative` | First person, change history, reviews, issues, commits, TODOs |
| `requirements` | Requirement and business-rule vocabulary |
| `commented-code` | Disabled source code |
| `count-claim`, `dated-claim`, `issue-ref`, `version-claim` | Unverifiable claims |
| `stale-path`, `stale-symbol`, `stale-adr` | Unresolved references |

The checker uses lexical heuristics; passing it does not establish the truth
of every sentence. Review still checks meaning and whether a comment is needed.
`node tools/comment-gate.cjs --json` emits findings with a failing exit status;
`--measure` prints statistics. There is no baseline that accepts existing debt.
12 changes: 12 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,15 @@ scan-prose:
@python3 tools/check_prose.py

check: spec-check parity probes versions scan-prose test

.PHONY: comments comments-test hooks
comments:
node tools/comment-gate.cjs

comments-test:
node --test tools/comment-gate.test.cjs

hooks:
git config core.hooksPath .githooks

test: comments
62 changes: 1 addition & 61 deletions go/plugin/capability.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,3 @@
/* Capabilities (§11.1).
*
* A DEPENDENCY IS ON A CAPABILITY, NOT ON A REF — because it is a
* dependency on something that can do the job, and which instance is
* doing it is exactly the configuration detail a plugin must not care
* about.
*
* But A BINDING IS TO AN INSTANCE, not to a capability, which is what
* decides behaviour when the bound provider leaves while another match
* remains. */

package plugin

import (
Expand All @@ -29,12 +18,7 @@ type Required struct {
Range string `json:"range,omitempty"`
Match map[string]any `json:"match,omitempty"`
Optional bool `json:"optional,omitempty"`
// Policy is §11.3: `static` restarts the consumer when its SELECTED
// provider leaves, even though another still matches; `dynamic` says
// in writing that it can survive the swap. Static is the default
// because most plugins cannot, and the cost of wrongly assuming they
// can is a live instance holding a dead reference.
Policy string `json:"policy,omitempty"`
Policy string `json:"policy,omitempty"`
}

type Candidate struct {
Expand All @@ -43,19 +27,6 @@ type Candidate struct {
Provides Provided `json:"provides"`
}

// ResolveCapability ranks the matching live providers and returns them
// best-first: highest `version`, then LOWEST `priority` (default 0),
// then declaration position `pos` ascending.
//
// `priority` is a field on the capability rather than §7's `order` band,
// because bands live on POINT BINDINGS: a provider may have several
// bindings with different bands, or none at all, so a rank reaching for
// one would be undefined in the common case.
//
// Without a total rank, "any provider satisfies" is true of the GRAPH
// and useless to the PLUGIN — two ports could bind different `store`
// instances, both resolve green, and behave differently, which is
// precisely the divergence a shared corpus exists to catch.
func ResolveCapability(req Required, candidates []Candidate) []Candidate {
hits := []Candidate{}
for _, c := range candidates {
Expand Down Expand Up @@ -115,10 +86,6 @@ func Matches(req Required, prov Provided) bool {
}
}

// `match` is checked against the provider's `attrs`, key by key. A
// key the provider does not carry is a miss, not a pass: a
// requirement asking for `transactional: true` must not be satisfied
// by a provider that never said.
if nil != req.Match {
attrs := prov.Attrs
if nil == attrs {
Expand Down Expand Up @@ -161,23 +128,6 @@ func parts(v string) []int {
return out
}

/* PARTIAL MATCH, RECURSING INTO MAPS (§11.1).
*
* §11.1 says `match` is "a partial match against `attrs`, with exactly
* the semantics voxgig/struct and the omni corpus already define for
* `match` — every leaf in the requirement must be present and equal in
* the capability, keys not mentioned are not checked."
*
* THE CANONICAL DID NOT IMPLEMENT THAT. It compared `attrs[k] !==
* req.match[k]`, which for any compound value is JavaScript reference
* identity: a requirement and a capability are declared in different
* places and are never the same object, so `match: {limits: {max: 5}}`
* could not be satisfied by ANY provider, including one declaring
* exactly that. The flat reading is invisible while every corpus entry
* is scalar, which is why P4 found it and P2 did not.
*
* Fixed in the canonical (AGENTS.md: change canonical first), pinned by
* `capability/nested`, and ported here. */
func matchvalue(want any, got any) bool {
switch w := want.(type) {

Expand Down Expand Up @@ -207,13 +157,6 @@ func matchvalue(want any, got any) bool {
return true

default:
// GO HAS MANY NUMBER TYPES AND THE MODEL HAS ONE. Corpus JSON
// decodes every number as float64, but a Go host declaring
// `Attrs: map[string]any{"max": 5}` writes an int, and `any(5)
// == any(5.0)` is false — so an attribute that matched in every
// other port silently missed here. Canonical is type-strict
// between KINDS (`true` never matches `1`), not between Go's
// spellings of one kind.
if wn, wok := numval(want); wok {
if gn, gok := numval(got); gok {
return wn == gn
Expand All @@ -224,9 +167,6 @@ func matchvalue(want any, got any) bool {
}
}

// numval reports a value's numeric value, and whether it had one. `bool`
// is deliberately absent: Go does not make it numeric and neither does
// the model.
func numval(v any) (float64, bool) {
switch n := v.(type) {
case int:
Expand Down
19 changes: 0 additions & 19 deletions go/plugin/catalog.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,5 @@
/* The definition catalog (§10.1).
*
* A definition is registered once and may back many instances. Option
* shapes are validated AT REGISTRATION, not when a document happens to
* exercise a key — so a malformed shape fails once, and in the same
* place everywhere (§9.4). */

package plugin

// Definition is what a plugin author writes.
//
// GO RETURNS ERRORS FROM LIFECYCLE CALLBACKS (§18, P4). The canonical
// throws; a Go callback returns `error`, and the host treats a non-nil
// return exactly as the canonical treats a throw — including
// `plugin_activate_failed`'s unwind and the `failed` status.
type Definition struct {
Name string
Shape any
Expand All @@ -27,12 +14,6 @@ type Catalog struct {
defs map[string]Definition
}

// MakeCatalog builds a catalog, adding each definition in turn.
//
// GO IS TIER S (§10.3): "package `init()` registration ... or an
// explicit list handed to makeHost", and both spellings land here. A Go
// developer's experience is "add the import, add one line to the config"
// rather than "write a factory".
func MakeCatalog(defs ...Definition) (*Catalog, error) {
c := &Catalog{defs: map[string]Definition{}}
for _, d := range defs {
Expand Down
Loading
Loading