Skip to content

docs(mdcode): rewrite the actions guide for a human reader - #427

Merged
libei merged 5 commits into
GoogleCloudPlatform:mainfrom
libei:humanize-actions-guide
Sep 15, 2026
Merged

libei merged 5 commits into
GoogleCloudPlatform:mainfrom
libei:humanize-actions-guide

Conversation

@libei

@libei libei commented Sep 15, 2026

Copy link
Copy Markdown
Collaborator

The actions guide passed the house-style checklist — sentence-case headings
within the length caps, spaced em dashes, no nested lists, no SHAG words — and
still read as machine-written. The checklist was never the failing thing. This
rewrites the prose for a reader who wants to declare an action and does not yet
know how.

What changed

The register. kcmd, push and the judge are the subjects of their own
sentences; the personified "we" is gone, 50 occurrences to 0. That matches the
rest of docs/semantic-model/, which uses "we" nowhere. Contractions stay,
and so do the long compound sentences — chopping those into staccato is itself
a tell.

The opening. The guide starts on what a reader can't do today: an agent
that can ask but not act, reaching writes through hand-written tools the model
never sees. The feature is named after the problem, not before it.

Prose that circled a point instead of stating it. This is the bulk of the
work and the class the review kept landing on — explaining by analogy, by
contrast, by rebuttal, or by abstraction before the reader has the thing
itself. Roughly sixty passages, across every section:

  • Sections that opened on a debate now open on the object. "An executor is
    opaque", three sentences on what a reader cannot see, and then "declare the
    blast radius" said twice, becomes "affects names the concepts a call
    writes to."
  • Rebuttals to designs nobody proposed are cut. "Asking you to repeat it would
    add a second place to get the answer wrong" says what concept takes
    instead.
  • Fourteen X, not Y antitheses become "rather than" or a plain sentence. An
    earlier pass in this branch introduced them; the construction appears
    nowhere in the reference prose it was supposed to match.
  • Wh-clefts where a plain verb works, colon splices gluing a claim to its own
    gloss, and prose sentences over forty words that carried more than one idea.
  • Abstract restatements of an example already on the page. "A store is typed
    by its backend:" led the field and the three values that immediately follow
    it; the field leads now.

Five factual corrections, each found by reading the source before moving
the text:

  • Section 1 claimed a push prunes actions the selected binding can't perform.
    It doesn't — kc_actions.ts publishes an entry for every action, with the
    executor fields absent when nothing is wired up. The true behaviour is now
    in section 5, where publishing is described.
  • on_violation read as though omitting it were legal with a default. It's a
    push error (validate.ts:520), and "an unmarked constraint rejects" is the
    reason the key is required, not a fallback.
  • "The gap is loud... and it's still a gap" conceded its own concession. A
    refusal being easy to spot and the rule still going unchecked are two
    separate facts; both are stated.
  • A rule variant that does not exist was described in the present tense.
  • The judge's read wrap is bounded per statement, but bounds neither which
    tables a read reaches nor a function that writes. Both limits are now
    stated where a reader would look for them.

What did not change

  • Every fenced block is byte-identical to main. All 564 lines of YAML,
    SQL, diagrams and captured CLI output, verified after every batch by
    diffing extracted fences.
  • One heading changed, ### What an entity-typed parameter adds to
    ### Parameters typed by an entity, because the original argued with
    hand-written tool schemas instead of saying what the thing is. It carries no
    inbound anchor. The five inbound anchors from reference.md,
    model_spec.md and the agent demo README all still resolve, as does every
    internal one.
  • No status note or stated limitation was weakened. The three "Status:"
    notes and "What is not modeled yet" carry the same content; five of them
    now carry more.

The guide was clean against the house-style checklist and still read as
machine-written. Rewrite the prose against the voice of the BigQuery
measures GA post: contractions, the product as the grammatical subject,
a problem-led opening, and no epigram closing every paragraph.

- Open on what the reader cannot do today, then name the feature, the
  way the blog spends its first paragraphs on the two-stack problem
  before naming measures.
- Drop the personified "we" for the CLI (51 occurrences to 0). kcmd,
  push and the judge are now the subjects of their own sentences; "we"
  no longer narrates the tool's own behaviour.
- Use contractions throughout, as the blog does.
- Cut the "X rather than Y" antithesis from 30 prose occurrences to 4,
  varying with "not", "instead of", or a restructured clause.
- Add one orienting line naming the running example.

Every fenced block is byte-identical to the previous revision: all 564
lines of YAML, SQL, diagrams and captured CLI output are unchanged, so
no listing drifts from what the commands actually print. Every heading
is unchanged, so the five inbound anchors from reference.md,
model_spec.md and the agent demo README still resolve.

No technical claim, status note or stated limitation was altered.
…tion

Four statements were wrong against the code a reader would act on:

  - "nothing consumes `affects` yet" — `generatedKeys` in run_action.ts walks
    `action.affects` and mints a UUID per `create` record, and
    `unusableGeneratedKey` refuses the call up front. The guide contradicted
    itself twice elsewhere (section 1's `@newTransferKey`, the withheld write
    tool).
  - `runtime.store.kind` was given as two-valued. `Store` is
    `SpannerStore|AlloyDbStore|BigQueryStore`, and `dataClientFor` serves both
    spanner and alloydb.
  - Section 7 gave the run target as Spanner only; the deployment target may be
    a Spanner or an AlloyDB database.
  - The identifying-text-field list omitted `fullname` and was stated
    case-sensitively; `identifyingColumn` tests
    /^(name|full_name|fullname|title|label|display_name)$/i and takes the first
    qualifying field in declaration order.

Also states what the guide had left unsaid: the strictest-outcome rule is not
what a run computes, since `askJudges` stops at the first non-advisory guard
that fails in declaration order and discards warnings collected before it; the
judge's four-read budget and what happens when it runs out; and that the
judge's SELECT/WITH wrap bounds neither a writing function under PostgreSQL nor
which tables a statement may name.

Prose changes cut five closing aphorisms, three restatements of the opening
thesis, the page's only blockquote and only Markdown table, and a rebuttal of a
design nobody proposed. Order 12345's total is corrected to its seeded $165.85.

Every fenced block stays byte-identical, every heading is unchanged, and the
five inbound anchors from reference.md, model_spec.md and the demo README still
resolve.
Five passages explained a point by contrast, analogy or abstraction before the
reader had the thing itself.

The worst was a claim that a push "behaves the way it already does for your
metrics", which compared to a feature the reader may not know, four sections
before `default_profile` is introduced. The behaviour it described does not
happen: `actionAspectData` in kc_actions.ts emits an aspect for every action,
and one with no executor publishes with the executor fields absent, which the
recovery path calls a legal published state. Nothing is pruned by binding. The
paragraph also restated the delete-reconciliation rule that section 5 already
states more precisely. Removed from section 1, with the true push behaviour
written into section 5 where the rest of the catalog-entry material lives.

  - "What an entity-typed parameter adds" opened by arguing with hand-written
    tool schemas. Retitled to name the concept, and it now says what one is
    before what it buys.
  - "Every entity and field therefore carries two names" restated in the
    abstract what the paragraph above had just shown as `Account`/`account`.
    Cut.
  - "Statements use your database names" led with a discrepancy the reader had
    to go back up to the listing to resolve. Reordered to rule, example,
    failure mode, and the verbatim-carry rationale is dropped here because the
    next passage argues it at length.
  - "On its own that's a catalogued rule and no more" said one thing in three
    sentences, two of them negative, and "acquires effect" appeared twice
    within twenty lines.

Also sweeps the class rather than the instances: fourteen `X, not Y` antithesis
appositions become `rather than` or plain sentences, four cleft constructions
are unwound, and the strictest-outcome sentence is split from sixty words into
three. Prose is reflowed to eighty columns.

Every fenced block stays byte-identical, the five inbound anchors still
resolve, and the one changed heading carries none.
Continues the directness pass through the first two sections, hunting the same
class the previous commit named.

One passage said two things that read as a contradiction. "A judgment has to
state `on_violation`" was followed by "an unmarked constraint rejects", which
implies omitting the key is legal and has a default. It isn't: validate.ts:520
makes omission a push error, and "an unmarked constraint rejects" is the reason
the key is required, quoted from the error text. Rewritten to say the push
fails and give that reason as a reason.

The rest are structural:

  - "Rule 3 is named like the rest, because an unnamed rule does nothing"
    opened by answering an objection nobody raised. The bold lead now states
    what rule 3 does and why.
  - "The listing below puts the two things your model has to settle beside each
    rule" announced a count whose members arrived only inside the table. It now
    names them.
  - Three colon splices become sentences, in the opaque-executor paragraph, the
    generated-key paragraph, and the second-call walkthrough.
  - Two "It's ..." clefts in consecutive sentences about outcome precedence
    become one plain causal sentence and one plain subject.
  - "Notice what stays computable alongside them:" directed the reader instead
    of telling them something.

Also repairs three ragged wraps the previous commit left behind, where a
replacement was rewrapped at its first line and pushed the overflow down.

Fenced blocks remain byte-identical, headings and inbound anchors unchanged,
and no prose line exceeds eighty columns.
Carries the same hunt into the second half of the guide: passages that circle
a point — by analogy, by contrast, by rebuttal, by abstraction — instead of
stating it. Thirty-two passages, each one verified against the source before
the text moved.

The structural ones:

  - Section 3 opened on "An executor is opaque", three sentences about what a
    reader cannot see, and then said "declare the blast radius" twice. It now
    opens on what `affects` names. The scope narrowed too, since a `sql`
    executor carries the write and is not opaque.
  - "`concept` names an entity or a relationship... Asking you to repeat it
    would add a second place to get the answer wrong" argued with a design
    nobody proposed. It states what `concept` takes.
  - "An agent needs two things... your entities already say what can be looked
    at and your actions already say what can be done" ran three clefts in two
    sentences. Entities supply the first, actions the second.
  - "A store is typed by its backend:" restated the field and its three values
    that follow it. The field leads.
  - "Drift costs you something in both directions" announced the shape of the
    two sentences after it without adding a fact.
  - Four more colon splices, five wh-clefts, and six sentences over forty words
    in the judge, runtime and agent-tool sections.

Two say something different than before, because the original was wrong about
its own subject. "The gap is loud... and it's still a gap" conceded its own
concession; a refusal being easy to spot and the rule still going unchecked are
two facts, so it now states both. And "The version that would hold regardless
counts the credits already on the order" described in the present tense a
version of the rule that does not exist.

Fenced blocks remain byte-identical to main, the inbound anchors from
reference.md, model_spec.md and the demo README all resolve, and no prose line
exceeds eighty columns.
@libei libei changed the title docs(mdcode): rewrite the actions guide in Cloud blog voice docs(mdcode): rewrite the actions guide for a human reader Sep 15, 2026
@libei
libei marked this pull request as ready for review September 15, 2026 17:15
@libei
libei merged commit df80a9d into GoogleCloudPlatform:main Sep 15, 2026
7 checks passed
libei added a commit to libei/knowledge-catalog that referenced this pull request Sep 15, 2026
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.
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