Skip to content

docs(mdcode): split section 2's example, correct section 3's claims about affects - #429

Merged
libei merged 4 commits into
GoogleCloudPlatform:mainfrom
libei:actions-guide-affects-section
Sep 15, 2026
Merged

libei merged 4 commits into
GoogleCloudPlatform:mainfrom
libei:actions-guide-affects-section

Conversation

@libei

@libei libei commented Sep 15, 2026

Copy link
Copy Markdown
Collaborator

Continues the sequential read-through of the merged actions guide. Two
sections, and the second one needed factual correction rather than
rewriting.

Section 2's spec and its worked example come apart

"A policy whose rules end differently" and "Two calls through that
policy" read as spec and as example at once, with no signal which was
which. The first opened on a five-rule business policy and then stopped
twice to state general rules about guards; the second walked a table of
outcomes that turned on one of those rules. A reader looking up how
guards compose had to read a credit example to find out, and a reader
following the example had to hold general machinery.

Three rules lived only inside the example, so anyone who skipped it never
learned them. The strictest-outcome precedence moves to "Naming a
constraint as a guard", which owns everything true of either constraint
kind. The warning an all-judged action loads with moves to "Rules no
query settles", beside the judged body it is about. What is left is an
example, so it becomes one: an unnumbered H2, "A credit policy, worked
through", skippable by its title the way "Carrying the write as DML" is.

The all-judged warning is also marked as current behavior rather than a
settled rule. warnAllGuardsJudged's own reason for warning instead of
erroring is that an all-judged action may be exactly what the author
meant, so the check fires on correct models; a reader who takes the
sentence as a rule will contort a model to avoid it. Whether a warning is
the right response is still open.

Section 3 was wrong about what affects drives

A read of the code behind affects refuted the section's two strongest
claims. "That key generation is the only thing affects drives" and "No
component computes an impact from it, routes on it, or checks it against
what your executor does" are both false on the same verb — affects
routes:

  • resolve_profiles.ts:466-475 drops an action from a profile-resolved
    model when affects names a concept the profile cannot bind, so on
    that push the action is never published at all.
  • kc_actions.ts:134-142 warns when a named concept has no entry in the
    push.

The status note now leads with the claim that survives — nothing compares
affects to what the executor does — and names the two places kcmd does
route on it.

Four more corrections in the same section:

  • Key generation is a sql feature, which the section never said
    while opening on an mcp executor. planFromExecutor refuses any
    other kind (run_action.ts:937-938).
  • The composite and non-String key check reads entities only.
    unusableGeneratedKey opens find(...); if (!entity) return null, so
    a relationship passes it, and an entity whose key names a field the
    model does not declare defaults to String and passes too. It runs at
    the call rather than at the push, which the section implied the other
    way round.
  • Transfer gets a String key. The guide teaches key generation by
    creating a Transfer, and the example model keyed transferId as an
    Integer — exactly what the paragraph below it says kcmd refuses,
    which made the section's own example unrunnable. This is the first of
    the three fenced-block defects recorded on docs(mdcode): second pass on the actions guide — state it once, state it plainly #428, fixed here because it
    is this section's example.
  • ir.ts claimed the generated key is "recorded as touched so the
    constraint probes cover the new row".
    Nothing records it, no such
    mechanism exists, and guards settle before the transaction opens, so no
    probe could observe a row that does not exist yet.

Section 3's lookup material gathers

concept, operation and fields were spread across two H3s and a
trailing paragraph with a modify aside between two of them. "What an
entry may say" now holds all three, and gains three rules the section
omitted: two entries on one concept with the same operation are a hard
load error, one bare entry beside one with an operation warns, and a
plain foreign-key edge has no fields of its own so naming one is always
an error — the guide's own TransferDebits is such an edge.

Two passages elsewhere depended on what section 3 got wrong

Section 4's "These checks are static, so they run on every push whatever
the destination" is not true of the concept and field checks.
validate.ts:219-221 builds the concept index only when fields are
unpruned, so a push resolving through a binding profile falls back to the
loader's warning, and only a catalog-only push or kcmd action run
treats those two as hard errors. Fields beside a delete read only the
entry, so that one does fail everywhere.

Section 7 stated "affects declares the blast radius rather than
limiting it" as if for the first time, four sections after affects was
taught. It now points back rather than re-deriving.

Key generation reconnects to the entry that causes it

"A created row gets its key from kcmd" arrived with nothing tying it to
the section around it. The heading's subject was a created row rather
than affects; the causal link was the paragraph's last sentence, after
the UUID, the binding name and the rationale; and the paragraph three
above it told the reader affects is checked "never against the
executor" and has "no way to confirm the write stays inside what you
named" — the status note's claim, stated twice, priming the reader to
expect nothing to happen.

The heading now names the cause in the vocabulary the slot rules just
taught — "Declaring a create turns on key generation" — the paragraph
opens on the link and moves its rationale below the example, the earlier
paragraph keeps only what the reader needs before the slot rules, and the
status note counts key generation as the first of the three things that
read affects.

Verification

Two fenced lines changed, both the Transfer key type, and the other 468
are byte-identical to main as a multiset. Prose stays inside 80
characters, no sentence either commit adds runs past 40 words, no
internal link broke, and the five inbound anchors from reference.md,
model_spec.md and the agent demo README resolve. Every changed line in
ir.ts is inside a doc comment.

The section 2 rename retires the anchor
#a-policy-whose-rules-end-differently, propagated to all three
references: two internal links and one from model_spec.md. Nothing
outside this repository links to it.

Two fenced-block defects from #428 remain for their own change: Figure 3
shows one UPDATE for a two-statement action and omits --arg target, and
a profile example is labelled commerce.profiles/operational.yaml inside
a model named payments.

The two sections that closed section 2 read as spec and as example at
once, with no signal which was which. "A policy whose rules end
differently" opened on a five-rule business policy, then stopped twice to
state general rules about `guards`, and "Two calls through that policy"
walked a table of outcomes that turned on one of those rules. A reader
looking up how `guards` compose had to read a credit example to find out,
and a reader following the example had to hold general machinery.

Three rules lived only inside the example. The strictest-outcome
precedence — any `reject` refuses, failing that any `escalate` holds,
failing that any `warn` reports — now sits in "Naming a constraint as a
guard", which owns everything true of either constraint kind. The warning
an all-judged action loads with now sits in "Rules no query settles",
beside the judged body it is about. Both are the answer to a question a
reader asks before reaching any example: what happens when one call
breaks several rules, and what does it cost to judge them all.

What is left is an example, so it becomes one. The two subsections merge
into an unnumbered H2, "A credit policy, worked through", skippable by
its title in the same way "Carrying the write as DML" is. Its commentary
on rule 3 stops re-deriving the inert-constraint rule and the
no-post-write rule, both stated earlier, and states the narrowing that is
specific to this policy: `IssueCredit` refuses to run against an order
whose books already disagree, and lets through a credit that would break
the agreement itself.

The heading rename propagates to all three references: the two internal
links later in the guide and one from `model_spec.md`. The anchor
`#a-policy-whose-rules-end-differently` no longer resolves anywhere, and
nothing outside this repository links to it.

No fenced block changed — all 470 fenced lines are byte-identical as a
multiset. Prose stays inside 80 characters, no internal link broke, and
the five inbound anchors resolve.
A read of the code behind `affects` refuted the section's two strongest
claims, and its own worked example turned out to be the case kcmd
refuses.

"That key generation is the only thing `affects` drives" and "No
component computes an impact from it, routes on it, or checks it against
what your executor does" are both false on the same verb. `affects`
routes. `resolve_profiles.ts:466-475` drops an action from a
profile-resolved model when `affects` names a concept the profile cannot
bind, so on that push the action is never published at all, and
`kc_actions.ts:134-142` warns when a named concept has no entry in the
push. The status note now leads with the claim that survives — nothing
compares `affects` to what the executor does — and names the two places
kcmd does route on it.

Key generation is a `sql` feature and the section never said so, while
opening on an `mcp` executor. `planFromExecutor` refuses any other kind
(`run_action.ts:937-938`), so an `mcp`, `rest` or `grpc` action declaring
a `create` gets nothing bound. The same omission was in `ir.ts`, which
also claimed the generated key is "recorded as touched so the constraint
probes cover the new row". Nothing records it: no such mechanism exists,
and guards settle before the transaction opens, so no probe could observe
a row that does not exist yet.

The composite and non-String key check reads entities only.
`unusableGeneratedKey` opens `const entity = ...find(...); if (!entity)
return null`, so a relationship passes it, and an entity whose key names
a field the model does not declare defaults to `String` and passes too.
It runs at the call rather than at the push, which the section implied
the other way round.

`Transfer` gets a String key. The guide teaches key generation by
creating a `Transfer`, and the example model keyed `transferId` as an
`Integer` — exactly what the paragraph below it says kcmd refuses, which
made the section's own example unrunnable. This is the first of the three
fenced-block defects recorded on GoogleCloudPlatform#428, and it is fixed here because it is
this section's example. `find_transfer`'s derived schema follows.

The three slot rules become one block. `concept`, `operation` and
`fields` were spread across two H3s and a trailing paragraph with a
`modify` aside between two of them, so a reader looking up what an entry
may contain had to cross the section. "What an entry may say" now holds
all three, and gains the three rules the section omitted: two entries on
one concept with the same operation are a hard load error, one bare entry
beside one with an operation warns, and a plain foreign-key edge has no
fields of its own so naming one is always an error. The guide's own
`TransferDebits` is such an edge.

Two passages elsewhere depended on what the section got wrong. Section
4's "These checks are static, so they run on every push whatever the
destination" is not true of the concept and field checks:
`validate.ts:219-221` builds the concept index only when fields are
unpruned, so a push that resolves through a binding profile falls back to
the loader's warning, and only a catalog-only push or `kcmd action run`
treats those two as hard errors. Fields beside a `delete` read only the
entry, so that one does fail everywhere. Section 7 stated "`affects`
declares the blast radius rather than limiting it" as if for the first
time, four sections after `affects` was taught; it now points back rather
than re-deriving.

Two fenced lines changed, both the `Transfer` key type, and the other 468
are byte-identical as a multiset. Prose stays inside 80 characters, no
sentence this change adds runs past 40 words, no internal link broke, the
five inbound anchors resolve, and every changed line in `ir.ts` is inside
a doc comment. The `, not just` antithesis in the opening paragraph goes
in the same pass.
…ses it

"A created row gets its key from kcmd" arrived with nothing connecting it
to the section it sits in. Three things put it there.

The heading's subject was a created row rather than `affects`, so it read
as a new topic in a section about declaring what a call changes. It now
names the cause in the vocabulary the reader just learned: "Declaring a
`create` turns on key generation".

The causal link was the paragraph's last sentence. The reader met the
UUID, the binding name and the reason an agent cannot pick its own keys
before being told what switches any of it on. The paragraph now opens on
the link — one entry in `affects` does more than describe the write — and
the rationale moves below the example.

The paragraph three above it primed the reader to expect nothing to
happen: "Each entry is checked against your ontology and never against
the executor ... has no way to confirm the write stays inside what you
named." That is the status note's claim, stated twice, and standing where
it stood it made key generation look like a contradiction. It now carries
only what the reader needs before the slot rules — that naming a field or
a concept records intent rather than constraining the write — and the
status note keeps the executor claim.

The status note also now accounts for key generation instead of leaving
it hanging: three things beyond the load checks read `affects`, and key
generation is the first of them.

No fenced block changed. Prose stays inside 80 characters, no sentence
this change adds runs past 40 words, no internal link broke, and the five
inbound anchors resolve.
"An action whose guards are all judged loads with a warning" read as a
settled rule, and it is a judgement call that may not hold. The loader's
own reason for making it a warning rather than an error is that it may be
exactly what the author meant -- some operations really are governed only
by rules no expression settles -- so the check fires on models that are
right. A reader who takes the sentence as a rule will contort a model to
avoid it.

The paragraph now says "today", and a second one states what the warning
is for and what it is not. It reports a cost in your source instead of
leaving you to find it in a published model, and whether a warning is the
right response to that cost is still open.

No fenced block changed, no code changed. Prose stays inside 80
characters, no sentence this adds runs past 40 words, and the five
inbound anchors resolve.
@libei
libei marked this pull request as ready for review September 15, 2026 22:26
@libei
libei merged commit 1404e59 into GoogleCloudPlatform:main Sep 15, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant