docs(mdcode): second pass on the actions guide — state it once, state it plainly - #428
Merged
libei merged 9 commits intoSep 15, 2026
Merged
Conversation
Three passages in "Statements use your database names" explained the rule by pointing somewhere else instead of stating it. Two of them were duplicating material the section already carries. The metric comparison is gone. "kcmd does translate a metric. You write `Account.balance`... an action's statements get no such pass" taught a second feature so the reader could infer a rule the bolded sentence above it already states outright, with the `Account` to `account` mapping shown in the example. What survives is the part that adds something: `@parameter` references are the only model names a statement may use. "Nothing catches a model name before the call" opened on an absence and then listed what validation does check — one DML verb, no `;`, declared parameters only — which is the "push holds you to it" list further down the same section. A reader hunting that list found it split across two places. The paragraph now says what happens when you get it wrong: nothing tells you until the action runs, the error comes from the store, and it can read like a fault in the statement rather than a typo in a name. That last phrasing is validate.ts's own, from the comment above validateRunnable. "Carrying the write, instead of pointing at it, buys you three things" made the reader decode that carrying means a `sql` executor and pointing means the other three, and it restated a contrast the section opening already drew forty lines earlier. It names the executor instead. No behaviour claim changed. Push validation really is static and target-independent, so it never asks the store whether a table exists; the fenced blocks are untouched and every anchor still resolves.
Four passages in the constraints sections said something the page had already said, which is what made them read as hard to digest rather than wrong. The "four stages" lead-in above Figure 2 duplicated the figure's own caption and restated, as an abstraction, the inert-constraint fact stated plainly two paragraphs above it. Figure 1 has no lead-in, and this figure does not need one either. "A constraint on its own does nothing. An action has to name it as a guard before anything checks it." was the third statement of that fact inside thirty-six lines, and it forward-referenced `guards`, which prose does not introduce for another twenty-five lines. The first two statements stay. The five habits for writing a judgment become a numbered list. They were five bold-led paragraphs under a sentence announcing a count, so the count and its members lived in different blocks and nothing tied a habit to its position. In "A policy whose rules end differently" the intro announced the table in almost the caption's own words, and the paragraph after it packed a statistic, the model's shape and the mapping rule into three unconnected sentences. "No query can settle two of the five" also made the reader go back to the table to learn which two, so the two judged rules are now named. "The second body" points at `judgment` by position over a set given far earlier in the guide, so it names `judgment` instead. No behaviour claim changed. All 564 fenced lines stay byte-identical to main, every heading is unchanged, and the five inbound anchors resolve.
"Model name" was never defined. The statements section opens on the concrete pair — the model's `Account` and `accountId` reach the store as `account` and `account_id` — but labels only the database half, then the next two paragraphs use "model names" as vocabulary. The guide also uses "model name" in a second, unrelated sense further down, where `--judge-model` takes the name of a Gemini model. Both paragraphs now say which side of the binding a name comes from instead of naming a category the reader has to construct. "Write a model name anywhere else and nothing tells you until the action runs" compounded that. It opens in the imperative, so it reads as an instruction to do the wrong thing, and "anywhere else" only resolves against the paragraph above. The subject is now a statement rather than a command, it reuses the `Account` and `account` from two paragraphs up, and it leads with the rule — push accepts it — before the consequence. "The judge" had the same problem one section down. "Writing a judgment" opens by saying a language model reads your sentence at review time, and eleven lines later starts calling that reader the judge, a term the guide never attaches to anything even though `--judge-model` and `--judge-reads-store` both depend on it. One clause now attaches it. No behaviour claim changed. All 564 fenced lines stay byte-identical to main, every heading is unchanged, and the five inbound anchors resolve.
… runtime doesn't implement The `sql` executor's example is a two-statement list, and nothing next to it said why an action would have more than one statement or what running them together guarantees. The transaction facts were in the guide, but roughly seven hundred lines downstream, in a figure caption and the run-time outcomes section, and the figure there shows a single statement between BEGIN and COMMIT, so a multi-statement list is never actually shown as one unit. A paragraph under the example now says it: one transaction, statements in the order written, committed at the end. Verified in run_action.ts — the transaction opens once at 212-222, the loop at 291-293 is a serial `for…of` over `plan.statements`, commit is a single call at 325-327, and any statement throwing lands in the rollback at 354-360. "The gate sees the write. The statements run where the constraints are probed, so a check observes the uncommitted result of the write it's gating." is not true of this runtime, and the guide already said so in two other places. Guards settle before the transaction exists (run_action.ts:181-198), there is no expression evaluator at all — an action guarded by one is refused rather than run (run_action.ts:529-535) — and nothing whatever runs between the last statement and the commit. Section 2 states the true version already: "Guards run before the transaction opens, so 'an order's total equals the sum of its lines' has nothing to look at yet." The bullet is gone and the count above it drops to two. Its neighbour overstated in the same direction. "A statement runs in the caller's own transaction and can be rolled back" names a transaction no external caller can supply — RunActionOptions has no way to pass one in, and the runner opens its own. A refusal also rolls nothing back, because it happens before there is a transaction. Both are now stated as they behave. An action that creates a row led with a prohibition and never named the mechanism. It now says kcmd generates the key, that the key is a UUID (run_action.ts:897, crypto.randomUUID), and that `affects` turns the generation on, before explaining why a caller-chosen key would be unsafe. The `affects` status note four hundred lines later no longer re-explains the same binding. All 564 fenced lines stay byte-identical to main, every heading is unchanged, and the five inbound anchors resolve.
A case-only difference is not an error. "A statement that says `Account` where it means `account`" would commit fine on both supported stores — GoogleSQL matches identifiers case-insensitively and PostgreSQL folds unquoted ones to lower case — so the sentence promised a failure that never happens. The example is now `accountId` against a column named `account_id`, which no folding rule reconciles. "`@parameter` references ... are the only names in a statement that come from your model" is contradicted by `@new<Concept>Key`, which is derived from a concept name and is not a declared parameter. validate.ts:282-286 adds it to `bindable` as a separate case for exactly that reason, and the guide's own create example binds one. The sentence now says what is true of both: a `@parameter` names a value kcmd binds at call time. The `affects` status note dropped a condition. The composite-key and non-String refusals fire only where a statement actually binds the generated key — run_action.ts:474 skips the check otherwise, with a comment saying an INT64-keyed entity whose DML supplies its own key must not be refused over a value it never uses. As written it read as unconditional, which would push a reader into deleting a correct `affects` record. "A guard becomes a real gate" lost the sentence that justified it when the false "gate sees the write" bullet went, leaving a headline about gating over a body about atomicity — which also restated the transaction fact a paragraph above. The body now states the gate property the runtime does have: every guard on a `sql` action settles before the transaction opens, so a refusal leaves the store untouched. Three smaller ones. "Rule 3 is the one rule here that a guard enforces more narrowly than it reads" claims a uniqueness rule 5 falsifies, so it no longer claims it. Reordering put the model description immediately before a colon and a YAML block showing constraints instead, so the clauses are swapped back. "The `on_violation` column" pointed at a column in a block that has keys. And "Claim no more in it than a judge can settle" made the pronoun reach past "All five" to "wording"; the noun is restored. All 564 fenced lines stay byte-identical to main, every heading is unchanged, and the five inbound anchors resolve.
`profiles.md` tells a profile author that the engine lowers each `expression` to the bound store's query language, and never says an action's statements are exempt. They are: kcmd passes a `sql` executor's statements to the store verbatim, which is why `actions.md` tells you to write physical table and column names in them. The file's own `CancelOrder` override depends on the exemption without stating it — line 325 writes `UPDATE Orders SET Status = 'CANCELLED'` against a model whose field is `key` and whose entity is `Order` — so a reader who takes the dialect note at its word writes logical names and gets a store error at the first call. A fourth note under `## Notes` states the rule and points at the block that relies on it. `reference.md` says "Every check here is static, so it runs on every push, regardless of destination" and stops, leaving the reader to work out what static excludes. The consequence is the one an action author actually hits: nothing in the push path asks the store a question, so a statement naming a table or a column that does not exist passes every check and fails when the action runs. One sentence now draws it, next to the sentence it qualifies. Neither file's headings change and no anchor moves. `actions.md` is untouched.
Three subsections of step 1 were about writing DML — "The executor is a
binding", "Writing the statements in the model" and "Statements use your
database names" — and concept material sat between them, so a reader who had
not yet decided how the write happens read physical detail twice before
finishing the declaration. Step 1 ran 229 lines to "## 2. Gate it with a
constraint" and three quarters of the span below its four-kind list was detail.
Step 1 now ends at the declaration and runs 106 lines: the name and parameters,
the model, the four executor kinds, entity-typed parameters, and where an
executor comes from. Everything about DML follows it under one unnumbered H2,
"## Carrying the write as DML", which a reader using `mcp`, `rest` or `grpc`
can skip by its title. Inside it the statements come first, then the names they
use, then which file to put them in — so the profile override fence now sits
under the rule it demonstrates instead of 80 lines above it.
Two facts left step 1 for the step that owns them. Key generation is an
`affects` fact, so it becomes "### A created row gets its key from kcmd" in
step 3, whose Status paragraph already said `affects` drives it; that paragraph
no longer re-explains the binding and opens on what it is actually reporting.
The three statement rules are push-time rules, so they become "### What push
holds a statement to" in step 4, beside the four static action errors that were
already there.
"The executor is a binding" is gone. Its rationale paragraph and its
inheritance rule restate `profiles.md:104-123`, which says it better —
"the same action, the same blast radius, performed by whatever mechanism the
bound store actually has". What step 1 keeps is the part a reader needs
there: the executor is physical, a profile can supply or replace it, and an
action a profile says nothing about keeps the model's.
The push rules move without being reworded. `validate.ts:299` tests
`text.slice(0, -1).includes(';')`, which strips the last character before
looking, so a trailing `;` passes and an interior one fails — exactly what "a
`;` inside a statement is rejected" says, and what `model_spec.md:490` and
`reference.md:458` say with "other than a trailing one". There was nothing to
correct.
No fenced line changed: all 478 are byte-identical to the ones on main, as a
multiset, with four blocks in new positions. No prose line exceeds 80
characters, the five frozen anchors resolve, and the one internal link into a
renamed heading now points at the new section.
A constraint carries one of two bodies, and section 2 taught them in an order that kept crossing between the two. An 88-line preamble opened on the concept, ran two expression examples, then spent twenty lines on `guards`, `on_violation` and the moment a guard is checked — machinery that belongs to both kinds — before "When no expression decides it" reached the judged body. A reader deciding which body to write had to carry all of it. The section now states the fork once, in the preamble, and then keeps the three subjects apart. "Naming a constraint as a guard" holds everything true of either kind: the `guards` list, why the reference sits on the action, when a guard is checked, why `guards` and `on_violation` are independent, and the push error and load warning that report a mismatch between them. "Rules a query settles" holds the `expression` body, what it can read, and how far a guard over stored data actually reaches. "Rules no query settles" holds the `judgment` body, followed by the existing "Writing a judgment". The two worked-example sections are unchanged and still take both kinds together. Two facts were stated once per kind and are now stated once. Both copies of the pre-write timing rule — "Every guard is checked before the call" in the expression material and "Guards run before the transaction opens" in the judged material — collapse into one sentence in the shared section, and each kind keeps only the consequence that is its own. The derived `evaluation` field was defined in the status note and again in the policy example; it is now defined in the preamble, where it makes the two-body distinction visible in the catalog, and the policy example just uses it. The status note split along the same line, because it was one paragraph reporting three unrelated states. What kcmd does with an expression sits under the expression section, what `--judge` does with a judgment sits under the judged section, and the run-time precedence gap stays with the strictest-outcome prose it qualifies. Eight lines of fenced YAML go. The `guards` example reprinted `TransferFunds` whole — executor, server URI, three parameters — to teach the one line `guards: [AmountIsPositive]`, for an action the reader has already seen declared twice. It now shows the constraint and that line. No other fenced line changed: the remaining 470 are byte-identical to main as a multiset. No prose line exceeds 80 characters, all five frozen anchors resolve, and no internal or inbound link broke.
Five are claims about kcmd that the code contradicts. "kcmd can run it rather than publishing it for another system to dispatch" made carrying the write an alternative to publishing. `kc_actions.ts:225-226` copies `sqlStatements` onto the published action verbatim, so a `sql` action is published like any other; what changes is who performs the write. The sentence now says that, and names the `TransferFunds` it is about to redeclare. "kcmd settles every guard on a `sql` action before it opens a transaction" overclaims twice. A guard it cannot settle is refused rather than settled (`run_action.ts:530-535` for an expression), and a `warn` guard left unsettled produces a warning rather than a refusal (`run_action.ts:199-204`). The bullet now claims only the gate property the runtime has: the guards settle before the transaction opens (`run_action.ts:180-204`, ahead of `withSession` at 212-222), and a call that does not clear them never reaches one. "Because `IssueCredit`'s other three guards are expressions, kcmd refuses it outright" names a cause that is never reached. `IssueCredit` has an `mcp` executor, so `whyRefusedWithoutRunning` stops at the executor-kind check (`run_action.ts:429`) before it ever looks at the guards. The sentence now states the outcome without attributing it to the wrong check. "Pass every value as a bound `@parameter` naming a parameter your action declares" omits `@new<Concept>Key`, which `validate.ts:282-286` seeds into `bindable` for every `create` in `affects`. Stated as written, the rule forbids the guide's own create example. The same omission was in `ir.ts`'s `SqlExecutor` comment and is fixed there too. `ir.ts` also still carried the "gate sees the write" bullet that GoogleCloudPlatform#427 removed from the guide: "the statements run where the constraints are probed, so the gate observes the uncommitted result of the write it is gating". Guards settle before the transaction exists, so nothing observes that. The bullet is gone and the count above it drops from three to two. Five are prose defects the restructure introduced or left behind. "Section 1 gave you an action that runs" was true before the DML material became its own section and now skips it. "Four things about an action can be statically wrong" reads as four things about the particular action on the page. "All of these checks are static" reaches back past an intervening list. "Expect no control flow" tells the reader to hold an expectation when the point is a property of `statements` itself. And moving the no-post-write paragraph left "however much it can read" pointing at the store-reading flag twenty lines ahead of it. Five findings were checked and rejected. `warn` guards are evaluated and reported (`run_action.ts:580`, 608-609, 622-623) rather than skipped, so the guide's existing wording holds. Figure 2's lead-in was deleted in GoogleCloudPlatform#427 on purpose and Figure 1 has none either. The unnumbered `## Carrying the write as DML` heading is the requested shape. The Integer-keyed `Transfer` is a known fenced-block defect recorded on the PR. And the semicolon rule is not factually wrong: `validate.ts:299` tests `text.slice(0, -1).includes(';')`, so a trailing `;` passes and an interior one fails, exactly as claimed. It was ambiguous rather than wrong, and now reads "anywhere but the end" to match `reference.md:458`. No fenced line changed. Prose stays inside 80 characters, the five frozen anchors resolve, and no internal or inbound link broke. Every changed line in `ir.ts` is inside a doc comment.
libei
marked this pull request as ready for review
September 15, 2026 19:34
libei
added a commit
to libei/knowledge-catalog
that referenced
this pull request
Sep 15, 2026
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.
libei
added a commit
that referenced
this pull request
Sep 15, 2026
…bout affects A sequential read-through of the merged guide reached sections 2 and 3. The first needed restructuring; the second turned out to be wrong about what `affects` does, so a read of the code behind it drove most of this change. Section 2's spec comes apart from its worked example. "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`, and the second walked a table of outcomes that turned on one of them. Three rules lived only inside the example, so a reader 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, and 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 rename retires the anchor `#a-policy-whose-rules-end-differently`, propagated to all three references. Section 3 was wrong about what `affects` drives. "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`, inside `pruneUnavailable`, 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 counts the three things that do read it. Four more corrections in the same section. Key generation is a `sql` feature the section never named 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, an entity whose key names an undeclared field defaults to `String` and passes too, and it runs at the call rather than at the push. `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. And `Transfer` gets a String key: the guide teaches key generation by creating a `Transfer`, and the example model keyed `transferId` as an `Integer`, which is exactly what the paragraph below it says kcmd refuses. 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. Key generation reconnects to the entry that causes it. Its heading named a created row rather than `affects`, its causal link was the paragraph's last sentence, and the paragraph three above it told the reader `affects` is checked "never against the executor", priming them to expect nothing to happen. The heading now names the cause in the vocabulary the slot rules just taught, the paragraph opens on the link and moves its rationale below the example, and the earlier paragraph keeps only what a reader needs before the slot rules. The all-judged warning is marked as current behavior. `warnAllGuardsJudged` warns rather than errors because an all-judged action may be exactly what the author meant, so the check fires on correct models, and a reader who takes the sentence as a rule will contort a model to avoid it. 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, and now points back rather than re-deriving. 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 this change adds runs past 40 words, no internal link broke, the five inbound anchors from `reference.md`, `model_spec.md` and the agent demo README resolve, and every changed line in `ir.ts` is inside a doc comment. 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`.
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.
Follow-up to #427. Reading the merged guide straight through turned up passages
that are clean by the checklist and still hard to get through. They fail in
three related ways: a passage explains a rule by pointing somewhere else
instead of stating it, it says something the page has already said, or it uses
a term of art the guide never defined. Two commits then act on the structural
version of the same complaint, one for each of the guide's two crossed-over
subjects, and the last one fixes what a second review found in all of it.
Nothing in the prose contradicts the code. Eight lines of fenced YAML were
deliberately deleted, named below; the other 470 are byte-identical to main as
a multiset, every frozen heading is unchanged, and the five inbound anchors from
reference.md,model_spec.mdand the agent demo README resolve.Each kind of constraint now has a section of its own
A constraint carries one of two bodies, and section 2 taught them in an order
that kept crossing between the two. An 88-line preamble opened on the concept,
ran two expression examples, then spent twenty lines on
guards,on_violationand the moment a guard is checked — machinery that belongs to both kinds —
before "When no expression decides it" reached the judged body. A reader
deciding which body to write had to carry all of it.
The section now states the fork once, in the preamble, and keeps the three
subjects apart afterwards. "Naming a constraint as a guard" holds everything
true of either kind: the
guardslist, why the reference sits on the action,when a guard is checked, why
guardsandon_violationare independent, andthe push error and load warning that report a mismatch between them. "Rules a
query settles" holds the
expressionbody, what it can read, and how far aguard over stored data actually reaches. "Rules no query settles" holds the
judgmentbody, followed by the existing "Writing a judgment". The twoworked-example sections are unchanged and still take both kinds together.
Two facts were stated once per kind and are now stated once. Both copies of the
pre-write timing rule — "Every guard is checked before the call" in the
expression material, "Guards run before the transaction opens" in the judged
material — collapse into one sentence in the shared section, and each kind keeps
only the consequence that is its own. The derived
evaluationfield was definedin the status note and again in the policy example; it is now defined in the
preamble, where it makes the two-body distinction visible in the catalog, and
the policy example only uses it.
The status note split along the same line, because it was one paragraph
reporting three unrelated states. What kcmd does with an expression sits under
the expression section, what
--judgedoes with a judgment sits under thejudged section, and the run-time precedence gap stays with the strictest-outcome
prose it qualifies.
The eight deleted fenced lines are the ones that made the
guardsexamplereprint
TransferFundswhole — executor, server URI, three parameters — toteach the single line
guards: [AmountIsPositive], for an action the reader hasalready seen declared twice. The example now shows the constraint and that line.
The DML material is now one section
Three subsections of step 1 were about writing DML — "The executor is a
binding", "Writing the statements in the model" and "Statements use your
database names" — with concept material between them, so a reader who had not
yet decided how the write happens met physical detail twice before finishing
the declaration. Step 1 ran 229 lines, and three quarters of the span below its
four-kind list was detail rather than concept or procedure.
Step 1 now ends at the declaration, at 106 lines: the name and parameters, the
model, the four executor kinds, entity-typed parameters, and where an executor
comes from. Everything about DML follows under one unnumbered H2, "##
Carrying the write as DML", skippable by its title for anyone using
mcp,restorgrpc. Inside it the statements come first, then the names they use,then which file to put them in — so the profile override fence sits under the
rule it demonstrates rather than 80 lines above it.
Two facts left step 1 for the step that owns them. Key generation is an
affectsfact, so it is now "### A created row gets its key from kcmd" in step3, whose Status paragraph already said
affectsdrives it; that paragraphstops re-explaining the binding and opens on what it actually reports. The
three statement rules are push-time rules, so they are now "### What push holds
a statement to" in step 4, beside the four static action errors already there.
"The executor is a binding" is gone. Its rationale and its inheritance rule
restate
profiles.md:104-123, which says it better — "the same action, thesame blast radius, performed by whatever mechanism the bound store actually
has." Step 1 keeps the part a reader needs there: the executor is physical, a
profile can supply or replace it, and an action a profile says nothing about
keeps the model's.
The push rules moved without being reworded.
validate.ts:299teststext.slice(0, -1).includes(';'), which strips the last character beforelooking, so a trailing
;passes and an interior one fails — exactly what "a;inside a statement is rejected" says, and whatmodel_spec.md:490andreference.md:458say as "other than a trailing one". There was nothing tocorrect.
A gate claim the runtime does not implement
"The gate sees the write" said a check observes the uncommitted result of
the write it is gating. Guards settle before the transaction exists
(
run_action.ts:181-198), there is no expression evaluator at all — an actionguarded by one is refused rather than run (
run_action.ts:529-535) — andnothing runs between the last statement and the commit. The guide already
stated the true version twice, including in section 2: "Guards run before the
transaction opens, so 'an order's total equals the sum of its lines' has
nothing to look at yet." The bullet is gone. It predates #427.
The statements and the transaction
The example is a two-statement list, and nothing beside it said why an action
would have more than one statement or what running them together guarantees.
The transaction facts were in the guide but roughly seven hundred lines
downstream, in a figure caption and the run-time outcomes section, and the
figure there shows a single statement between
BEGINandCOMMIT, so amulti-statement list is never shown as one unit. A paragraph under the example
now says it: one transaction, statements in the order written, committed at the
end. Verified in
run_action.ts— the transaction opens once at 212-222, theloop at 291-293 is a serial
for…ofoverplan.statements, commit is a singlecall at 325-327, and any statement throwing lands in the rollback at 354-360.
"A statement runs in the caller's own transaction and can be rolled back" names
a transaction no external caller can supply:
RunActionOptionshas no way topass one in, and the runner opens its own. A refusal also rolls nothing back,
because it happens before there is a transaction.
An action that creates a row led with a prohibition and never named the
mechanism. It now says kcmd generates the key, that the key is a UUID
(
run_action.ts:897,crypto.randomUUID), and thataffectsturns thegeneration on, before explaining why a caller-chosen key would be unsafe.
Physical names, and what was never defined
The metric comparison is gone. "kcmd does translate a metric. You write
Account.balance… an action's statements get no such pass" taught a secondfeature so the reader could infer a rule the bolded sentence above it already
states outright.
"Nothing catches a model name before the call" opened on an absence and then
listed what validation does check, which is the push-rule list further down the
same section. A reader hunting that list found it split across two places. The
paragraph now says what happens when you get it wrong: nothing tells you until
the action runs, the error comes from the store, and it can read like a fault
in the statement rather than a typo in a name. That last phrasing is
validate.ts's own, from the comment abovevalidateRunnable."Carrying the write, instead of pointing at it, buys you three things" made the
reader decode that carrying means a
sqlexecutor and pointing means the otherthree, and it restated a contrast the section opening already drew forty lines
earlier.
"Model name" was never defined. The section opened on the concrete pair — the
model's
AccountandaccountIdreach the store asaccountandaccount_id— but labelled only the database half, then used "model names" as vocabulary.
The guide also uses "model name" in a second, unrelated sense further down,
where
--judge-modeltakes the name of a Gemini model."Write a model name anywhere else and nothing tells you until the action runs"
compounded that: it opens in the imperative, so it reads as an instruction to
do the wrong thing, and "anywhere else" only resolves against the paragraph
above. The subject is now a statement rather than a command, and it leads with
the rule before the consequence.
Constraints
The "four stages" lead-in above Figure 2 duplicated the figure's own caption
and restated, as an abstraction, the inert-constraint fact stated plainly two
paragraphs above it. Figure 1 has no lead-in and this one does not need one.
"A constraint on its own does nothing. An action has to name it as a guard
before anything checks it." was the third statement of that fact inside
thirty-six lines, and it forward-referenced
guards, which prose does notintroduce for another twenty-five lines. The first two statements stay.
The five habits for writing a judgment become a numbered list. They were five
bold-led paragraphs under a sentence announcing a count, so the count and its
members lived in different blocks and nothing tied a habit to its position.
"The judge" was the other undefined term. "Writing a judgment" opens by saying
a language model reads your sentence at review time, then eleven lines later
starts calling that reader the judge, which the guide never attaches to
anything even though
--judge-modeland--judge-reads-storeboth depend onit. One clause now attaches it.
The policy table's intro announced the table in almost the caption's own words,
and "no query can settle two of the five" made the reader go back to the table
to learn which two, so the two judged rules are named. "The second body" points
at
judgmentby position over a set given far earlier, so it namesjudgment.Two gaps in the sibling docs
profiles.mdtells a profile author the engine lowers eachexpressionto thebound store's query language, and never says an action's statements are exempt.
They are — kcmd passes them to the store verbatim, which is why
actions.mdtells you to write physical names in them. The file's own
CancelOrderoverride depends on the exemption without stating it, writing
UPDATE Orders SET Status = 'CANCELLED'against a model whose entity isOrderand whosefield is
key. A fourth note under## Notesstates the rule.reference.mdsays "Every check here is static, so it runs on every push,regardless of destination" and stops. One sentence now draws the consequence an
action author actually hits: nothing in the push path asks the store a
question, so a statement naming a table or a column that does not exist passes
every check and fails when the action runs.
What the first review caught
Eight defects, six introduced by the edits above.
A case-only difference is not an error. "A statement that says
Accountwhereit means
account" would commit fine on both supported stores — GoogleSQLmatches identifiers case-insensitively and PostgreSQL folds unquoted ones to
lower case — so the sentence promised a failure that never happens. The example
is now
accountIdagainst a column namedaccount_id."
@parameterreferences … are the only names in a statement that come fromyour model" is contradicted by
@new<Concept>Key, which is derived from aconcept name and is not a declared parameter.
validate.ts:282-286adds it tobindableas a separate case for exactly that reason.The
affectsstatus note dropped a condition. The composite-key andnon-
Stringrefusals fire only where a statement actually binds the generatedkey —
run_action.ts:474skips the check otherwise, so that an INT64-keyedentity whose DML supplies its own key is not refused over a value it never
uses. As written it read as unconditional, which would push a reader into
deleting a correct
affectsrecord."A guard becomes a real gate" lost the sentence that justified it when the
false "gate sees the write" bullet went, leaving a headline about gating over a
body about atomicity. The body now states the gate property the runtime does
have: every guard on a
sqlaction settles before the transaction opens, so arefusal leaves the store untouched.
Three smaller ones. "Rule 3 is the one rule here that a guard enforces more
narrowly than it reads" claims a uniqueness rule 5 falsifies. Reordering put a
model description immediately before a colon and a YAML block showing
constraints instead. "The
on_violationcolumn" pointed at a column in a blockthat has keys. And "Claim no more in it than a judge can settle" made the
pronoun reach past "All five" to "wording".
What the second review caught
Ten defects. Five are claims about kcmd that the code contradicts, and the
ir.tschanges below are comment-only — every changed line in that file isinside a doc comment.
"kcmd can run it rather than publishing it for another system to dispatch" made
carrying the write an alternative to publishing.
kc_actions.ts:225-226copiessqlStatementsonto the published action verbatim, so asqlaction ispublished like any other and what changes is who performs the write.
"kcmd settles every guard on a
sqlaction before it opens a transaction"overclaims twice. A guard kcmd cannot settle is refused rather than settled
(
run_action.ts:530-535for an expression), and awarnguard left unsettledproduces a warning rather than a refusal (
run_action.ts:199-204). The bulletnow claims only the gate property the runtime has: the guards settle before the
transaction opens (
run_action.ts:180-204, ahead ofwithSessionat 212-222),and a call that does not clear them never reaches one.
"Because
IssueCredit's other three guards are expressions, kcmd refuses itoutright" names a cause never reached.
IssueCredithas anmcpexecutor, sowhyRefusedWithoutRunningstops at the executor-kind check(
run_action.ts:429) before it looks at the guards."Pass every value as a bound
@parameternaming a parameter your actiondeclares" omits
@new<Concept>Key, whichvalidate.ts:282-286seeds intobindablefor everycreateinaffects. As written the rule forbids theguide's own create example. The same omission was in
ir.ts'sSqlExecutorcomment.
ir.tsalso still carried the "gate sees the write" bullet that #427 removedfrom the guide — "the statements run where the constraints are probed, so the
gate observes the uncommitted result of the write it is gating". Guards settle
before the transaction exists, so nothing observes that. The bullet is gone and
the count above it drops from three to two.
Five are prose defects the restructure introduced or left behind. "Section 1
gave you an action that runs" was true before the DML material became its own
section. "Four things about an action can be statically wrong" reads as four
things about the particular action on the page. "All of these checks are
static" reaches back past an intervening list. "Expect no control flow" tells
the reader to hold an expectation when the point is a property of
statements.And moving the no-post-write paragraph left "however much it can read" pointing
at the store-reading flag twenty lines ahead of it.
Five findings were checked and rejected.
warnguards are evaluated andreported (
run_action.ts:580, 608-609, 622-623) rather than skipped, so theexisting wording holds. Figure 2's lead-in was deleted in #427 on purpose and
Figure 1 has none either. The unnumbered
## Carrying the write as DMLheadingis the requested shape. The Integer-keyed
Transferis the known defectrecorded below. And the semicolon rule is not factually wrong:
validate.ts:299tests
text.slice(0, -1).includes(';'), so a trailing;passes and aninterior one fails, exactly as claimed. It was ambiguous rather than wrong, and
now reads "anywhere but the end" to match
reference.md:458.Known defects not fixed here
Three live inside fenced blocks, which this branch has otherwise left
byte-identical:
Transfer.transferIdisdatatype: Integer, but the create example binds@newTransferKeyand the runtime generates a UUID string, sounusableGeneratedKey(run_action.ts:903-925) refuses it. The guide's onlykey-generation example is the one example that cannot run. Fixing it ripples
into the captured agent-tool listing, which should come from a live run.
TransferFundswithsourceandamount, omitting therequired
target, and shows oneUPDATEbetweenBEGINandCOMMITfor anaction that declares two statements.
# commerce.profiles/operational.yamlbutthe model inside it is
payments; the convention everywhere else is<model>.profiles/<name>.yaml.