docs(mdcode): split section 2's example, correct section 3's claims about affects - #429
Merged
libei merged 4 commits intoSep 15, 2026
Merged
Conversation
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
marked this pull request as ready for review
September 15, 2026 22:26
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 ofoutcomes that turned on one of those rules. A reader looking up how
guardscompose had to read a credit example to find out, and a readerfollowing 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 oferroring 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
affectsdrivesA read of the code behind
affectsrefuted the section's two strongestclaims. "That key generation is the only thing
affectsdrives" and "Nocomponent computes an impact from it, routes on it, or checks it against
what your executor does" are both false on the same verb —
affectsroutes:
resolve_profiles.ts:466-475drops an action from a profile-resolvedmodel when
affectsnames a concept the profile cannot bind, so onthat push the action is never published at all.
kc_actions.ts:134-142warns when a named concept has no entry in thepush.
The status note now leads with the claim that survives — nothing compares
affectsto what the executor does — and names the two places kcmd doesroute on it.
Four more corrections in the same section:
sqlfeature, which the section never saidwhile opening on an
mcpexecutor.planFromExecutorrefuses anyother kind (
run_action.ts:937-938).unusableGeneratedKeyopensfind(...); if (!entity) return null, soa relationship passes it, and an entity whose key names a field the
model does not declare defaults to
Stringand passes too. It runs atthe call rather than at the push, which the section implied the other
way round.
Transfergets a String key. The guide teaches key generation bycreating a
Transfer, and the example model keyedtransferIdas anInteger— 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.tsclaimed the generated key is "recorded as touched so theconstraint 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,operationandfieldswere spread across two H3s and atrailing paragraph with a
modifyaside between two of them. "What anentry 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
TransferDebitsis 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-221builds the concept index only when fields areunpruned, so a push resolving through a binding profile falls back to the
loader's warning, and only a catalog-only push or
kcmd action runtreats those two as hard errors. Fields beside a
deleteread only theentry, so that one does fail everywhere.
Section 7 stated "
affectsdeclares the blast radius rather thanlimiting it" as if for the first time, four sections after
affectswastaught. 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, afterthe UUID, the binding name and the rationale; and the paragraph three
above it told the reader
affectsis checked "never against theexecutor" 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
createturns on key generation" — the paragraphopens 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
Transferkey type, and the other 468are byte-identical to
mainas a multiset. Prose stays inside 80characters, no sentence either commit adds runs past 40 words, no
internal link broke, and the five inbound anchors from
reference.md,model_spec.mdand the agent demo README resolve. Every changed line inir.tsis inside a doc comment.The section 2 rename retires the anchor
#a-policy-whose-rules-end-differently, propagated to all threereferences: two internal links and one from
model_spec.md. Nothingoutside 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, anda profile example is labelled
commerce.profiles/operational.yamlinsidea model named
payments.