Repository navigation
docs(agents): the Playwright specs have been in CI since 2026-08-08 - #768
Merged
Merged
Conversation
AGENTS.md's #394 rule and its decision record both said tools/simulation/k6/ and the Playwright specs in tools/simulation/ui/ were uncovered by CI. Half of that stopped being true: e2e-smoke.yml runs the quick suite on pull_request, path-filtered to src/**, web/**, tools/simulation/** and deploy/**, so a write-contract change — which lives in src/ by definition — does run them. k6 is still workflow_dispatch only, and everything about its tolerated-status list hiding a broken write is unchanged. The error pointed the safe way, so nothing shipped broken. It cost misdirected effort: on #727 the Playwright half of the caller read was done by hand under the belief a green baseline would hide a break. It also flattened the one caller that still needs reading into a list of two. The decision record gets a dated amendment rather than a rewrite, because what it recorded was true when written. Closes #767
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
mforce
pushed a commit
that referenced
this pull request
Sep 12, 2026
🤖 I have created a release *beep* *boop* --- ## [0.1.0](v0.0.4...v0.1.0) (2026-09-12) ### ⚠ BREAKING CHANGES * log in by farm code, with per-account email identity ([#532](#532)) (#564) ### Features * **accounts:** add Account.Slug (farm code), suspend/reactivate, list-accounts verb ([#531](#531)) ([3fe9754](3fe9754)) * **accounts:** provision additional farms ([#581](#581)) ([006f298](006f298)) * add Aspire local development AppHost ([#567](#567)) ([2c9e6b9](2c9e6b9)) * add configurable worker sale allocation ([#619](#619)) ([0955095](0955095)) * add searchable entity pickers ([#642](#642)) ([60d2053](60d2053)) * **api:** provision-account takes an optional --timezone at creation ([#603](#603)) ([#694](#694)) ([a0aee39](a0aee39)) * **audit:** show the sales-line audit payload as a readable Details column ([#745](#745)) ([#749](#749)) ([d26d389](d26d389)) * **auth:** add ApplicationUser.StepUpLogoutEpoch column ([#338](#338)) ([#554](#554)) ([18306ee](18306ee)) * certify over-cap simulation fixture bands ([#633](#633)) ([a67b2e1](a67b2e1)), closes [#627](#627) * **cli:** rename-account verb to change a farm code ([#732](#732)) ([#733](#733)) ([4b70559](4b70559)) * **customers:** edit existing customer details ([#625](#625)) ([#626](#626)) ([062a55c](062a55c)) * **jobs:** single-runner leader gate for the durable job worker ([#271](#271)) ([#555](#555)) ([4148f9b](4148f9b)) * let owners change user email addresses ([#605](#605)) ([842347b](842347b)) * log in by farm code, with per-account email identity ([#532](#532)) ([#564](#564)) ([68adb62](68adb62)) * **ratelimit:** distributed IP-keyed auth limiters ([#544](#544)) ([#558](#558)) ([ec14972](ec14972)) * **ratelimit:** distributed per-account report concurrency cap with local-ceiling fallback ([#545](#545)) ([#559](#559)) ([1522e4e](1522e4e)) * **sales:** mark discounted lines, total the discount, and show it in the Orders list ([#723](#723), [#724](#724)) ([#741](#741)) ([1a07441](1a07441)) * **sales:** record list, old and new price in the order-line audit payload ([#722](#722)) ([#742](#742)) ([97c866f](97c866f)) * **sales:** refuse an over-ceiling confirm from a Sales user ([#727](#727)) ([#766](#766)) ([8c0792a](8c0792a)) * **sales:** show what each order still owes, and filter the list to unpaid ([#771](#771)) ([ca59d68](ca59d68)) * **sales:** snapshot the list price on the order line and show the discount ([#734](#734)) ([cffed5e](cffed5e)) * **sales:** snapshot the product name and unit in the order-line audit payload ([#747](#747)) ([#748](#748)) ([0481c06](0481c06)) * scope Worker reads to assigned flocks ([#388](#388)) ([#611](#611)) ([5884a9a](5884a9a)) * shared-state ports with Redis + in-process fallback ([#543](#543)) ([#552](#552)) ([f767fa9](f767fa9)) * suspend-account / reactivate-account operator verbs ([#534](#534)) ([#573](#573)) ([d0be26c](d0be26c)) * **tenancy:** write-side tenant guard + single-assignment TenantContext ([#546](#546)) ([#561](#561)) ([f371f1d](f371f1d)) * **web:** dashboard rework — capture-status tiles, 14-day trend, stock as a stacked bar ([#654](#654)) ([396ba23](396ba23)) * **web:** date-range filters on audit and expenses, and the stock lot filter gets its bounded toolbar ([#666](#666), [#667](#667), [#653](#653)) ([94b188f](94b188f)) * **web:** elevation hierarchy and sentence-case labels ([#651](#651), [#652](#652)) ([#661](#661)) ([28db4c7](28db4c7)) * **web:** Expenses and Audit keep a clear-filters control while rows are still showing ([#679](#679)) ([#697](#697)) ([b859982](b859982)) * **web:** expenses filters by a date range like its sibling screens ([#667](#667)) ([f13858f](f13858f)) * **web:** key the farm brand palette per farm ([#586](#586)) ([#600](#600)) ([7183a43](7183a43)) * **web:** let operators forget remembered farms ([#598](#598)) ([577d94e](577d94e)) * **web:** one-line provenance, bounded date filters, and empty states that invite action ([#653](#653), [#655](#655)) ([#668](#668)) ([80b53f4](80b53f4)) * **web:** prefill the farm code from ?farm= and remember it ([#535](#535)) ([#588](#588)) ([b7f5cc6](b7f5cc6)) * **web:** split authenticated routes into lazy chunks ([#620](#620)) ([5089271](5089271)) * **web:** the audit log filters by a date range, and says which window is empty ([#666](#666)) ([63027e0](63027e0)) * **web:** typeset numbers as numbers and refresh the Help glossary ([#650](#650), [#657](#657)) ([af4fe11](af4fe11)) ### Bug fixes * **api:** order same-instant audit events by a durable monotonic key ([#700](#700)) ([8fcf084](8fcf084)) * **api:** print the farm code from bootstrap-admin ([#589](#589)) ([#594](#594)) ([34032ac](34032ac)) * **audit:** show the price a line sold for, not its list price ([#759](#759)) ([e6b37d0](e6b37d0)) * **audit:** store catalog enums by name and guard the add-item transaction shape ([#751](#751)) ([23609ff](23609ff)) * **auth:** reject invalid account claims ([#622](#622)) ([8d6c7fe](8d6c7fe)) * **auth:** require step-up for durable user access ([#360](#360)) ([#607](#607)) ([f767dce](f767dce)) * **ci:** bound the npm audit calls and give the web job room to finish ([#686](#686)) ([153b7a8](153b7a8)) * **ci:** escalate the audit bound to SIGKILL, so it actually bounds ([#686](#686)) ([a0c8f4e](a0c8f4e)) * **ci:** fail closed on invalid vulnerability config ([#621](#621)) ([1690db8](1690db8)) * **ci:** lockfix covers the two AppHost lock files, derived from the sln ([efb05e6](efb05e6)) * **ci:** lockfix covers the two AppHost lock files, derived from the sln ([8986d77](8986d77)) * **ci:** remove invalid XML comment from nuget.lockfix.config ([#541](#541)) ([5f1bc0a](5f1bc0a)) * **ci:** the advisory vuln gate no longer blocks on an unusable report ([#686](#686)) ([aaf6934](aaf6934)) * **ci:** the advisory vuln gate no longer blocks on an unusable report ([#686](#686)) ([64f1f53](64f1f53)) * **i18n:** tl help text names the saleable flag and unit-system setting what their labels call them ([#688](#688)) ([#696](#696)) ([bfd24d7](bfd24d7)) * **infra:** AccountId must be a non-nullable Guid or both tenant write layers refuse ([#673](#673)) ([#695](#695)) ([2470c4e](2470c4e)) * require step-up for flock scope changes ([#609](#609)) ([4151f89](4151f89)) * **sales:** keep a line's discount markers agreeing while its price is edited ([#752](#752)) ([#753](#753)) ([c159b4b](c159b4b)) * **sales:** say which kind of missing list price a line has ([#774](#774)) ([489180e](489180e)) * scope legacy logout to selected farm ([#624](#624)) ([fae8d82](fae8d82)) * **seed:** drain the daily-entry lock sweep so deep simulation fixtures validate ([#644](#644)) ([730fa23](730fa23)), closes [#638](#638) * **tenancy:** AccountId is a concurrency token, so the database refuses a detached cross-tenant write ([#562](#562)) ([4d1dfa3](4d1dfa3)) * **tenancy:** AspNetUserRoles carries a tenant column, so a role write naming another farm's user is refused ([#670](#670)) ([fc0552a](fc0552a)) * **tests:** bump the image-pin allow-list counts for the AppHost LocalPorts tests ([#593](#593)) ([58d3056](58d3056)) * **tests:** the OTLP collector survives a lost port race and ignores traffic that is not an export ([#672](#672), [#676](#676)) ([#677](#677)) ([965c737](965c737)) * **web:** a scoped audit view filtered to nothing names both the record and the range ([#666](#666)) ([41bbfe1](41bbfe1)) * **web:** an abandoned dialog attempt's success no longer hijacks the replacement on Customers, Daily Entry, Flocks, Grades and Products ([#703](#703)) ([#705](#705)) ([85605db](85605db)) * **web:** an abandoned dialog attempt's success no longer hijacks the replacement on Inventory, Expenses, History and Stock ([#703](#703)) ([#706](#706)) ([60a4997](60a4997)) * **web:** an abandoned edit's success no longer hijacks the dialog that replaced it on Users ([#703](#703)) ([#710](#710)) ([778faab](778faab)) * **web:** an abandoned order attempt's success no longer hijacks the dialog that replaced it ([#702](#702)) ([522c699](522c699)) * **web:** capture screens open on the flock you last used, and assigning one no longer guesses ([#646](#646)) ([#699](#699)) ([7f8f317](7f8f317)) * **web:** constrain dialog session helpers to declared scopes ([#715](#715)) ([389e3c8](389e3c8)) * **web:** date validation gets one boundary table instead of one case per review round ([#666](#666)) ([215f830](215f830)) * **web:** keep a paged window and an item panel on the user's newest intent ([#645](#645)) ([d81bccf](d81bccf)) * **web:** keep Sales order panels closed after pending writes ([#711](#711)) ([f0f7492](f0f7492)) * **web:** keep Sales panels closed after pending Open reads ([#716](#716)) ([620411f](620411f)) * **web:** make login take the cross-tab cookie lock so a racing refresh cannot restore the wrong session ([#648](#648)) ([ff18beb](ff18beb)) * **web:** make the entity picker read as a search field and focus it on open ([#736](#736)) ([66ef667](66ef667)), closes [#735](#735) * **web:** page truncated customer and movement tables with usePagedList ([7cfe4d6](7cfe4d6)) * **web:** reconcile Sales line edits with refreshed orders ([#717](#717)) ([d7dd2c9](d7dd2c9)) * **web:** the audit date filter accepts low-numbered years, and its empty state covers every narrowing ([#666](#666)) ([af52d25](af52d25)) * **web:** the audit date filter rejects impossible dates, and its history guard actually guards ([#666](#666)) ([8d51846](8d51846)) * **web:** the expense range bounds are not capped at today, which the month-end default exceeds ([#667](#667)) ([7e01864](7e01864)) * **web:** the help text calls the expiry field what the field calls itself ([#666](#666)) ([2fd1f3c](2fd1f3c)) * **web:** the stock lot date range sits in the bounded toolbar ([#653](#653)) ([43dec5e](43dec5e)) ### Refactoring * **web:** extract SalesPage's dialog-write wrapper into a shared useDialogAction hook ([#703](#703)) ([#704](#704)) ([60ee9d9](60ee9d9)) ### Documentation * add k6 preparation steps to the dev-database fixture runbook ([#643](#643)) ([a4f1f09](a4f1f09)) * add runbook for loading the simulation fixture into a dev database ([#639](#639)) ([2d143b8](2d143b8)) * **agents:** a PR closes its issue from the body, not the title ([#744](#744)) ([39be13c](39be13c)) * **agents:** drop the commit and push gate, and require screenshots on UI changes ([#757](#757)) ([6225172](6225172)) * **agents:** find guards by grepping registry readers; amend issues a PR overtakes ([#580](#580)) ([fe3fde8](fe3fde8)) * **agents:** the Playwright specs have been in CI since 2026-08-08 ([#768](#768)) ([68ee612](68ee612)) * **aspire:** record the second local database and pin the AppHost dashboard ports ([#623](#623)) ([713b941](713b941)) * compress AGENTS.md to one paragraph per rule, and draw the two orders that matter ([#551](#551)) ([997ae8a](997ae8a)) * item 7 names each screen's actual initial filter value ([#666](#666)) ([70a53d8](70a53d8)) * multi-farm tenancy decision record and AGENTS/GLOSSARY sync ([#537](#537)) ([#601](#601)) ([2c34771](2c34771)) * name the scoped filtered-empty key and state the [#653](#653) relationship plainly ([#666](#666)) ([0e93dac](0e93dac)) * note that a PackageReference in Directory.Build.props is invisible to the dependency graph ([4845724](4845724)) * **plans:** commit the [#722](#722) and [#745](#745) design records ([#754](#754)) ([c942fcd](c942fcd)) * record [#579](#579) as won't-fix — suspension is immediate for use, not issuance ([#582](#582)) ([7a3be40](7a3be40)) * record the [#508](#508) audit ordering key and the tracked-file guard lesson ([#701](#701)) ([08964e9](08964e9)) * **runbooks:** add procedure to rename the default farm's code after upgrade ([#731](#731)) ([2f6e242](2f6e242)) * screenshots of the running SPA in the README ([#550](#550)) ([711488a](711488a)) * **sim:** commit the dashboard screenshot, capture the palette matrix, and record the [#651](https://github.com/mforce/cluckwork/issues/651)/[#652](https://github.com/mforce/cluckwork/issues/652) conventions ([#660](#660), [#662](#662), [#663](#663), [#664](#664)) ([#665](#665)) ([930ea30](930ea30)) * specify searchable entity picker ([#641](#641)) ([91d4300](91d4300)) * split the README into audience-scoped docs and adopt repo-template scaffolding ([#548](#548)) ([b3f3fcf](b3f3fcf)) * surface Aspire local development workflow ([#568](#568)) ([a343baa](a343baa)) * **web:** record the per-screen idempotency-key policies and runWrite's refresh contract ([#703](#703)) ([#707](#707)) ([8bee651](8bee651)) * **web:** the date-cap help text covers every stocked item, not only feed ([#666](#666), [#667](#667)) ([c8433c5](c8433c5)) * **web:** the help text claims only what is true of recording, and says nothing about filter caps ([#666](#666), [#667](#667)) ([e2f63d1](e2f63d1)) * **web:** the help text describes the date-range filters that shipped ([#666](#666), [#667](#667)) ([c3275b7](c3275b7)) * **web:** the help text stops describing a cap the filters no longer have ([#666](#666), [#667](#667)) ([49654cd](49654cd)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: cluckwork-lockfix[bot] <309265648+cluckwork-lockfix[bot]@users.noreply.github.com>
This was referenced Sep 12, 2026
mforce
added a commit
that referenced
this pull request
Sep 12, 2026
…uests (#783) Skips `Image build + Trivy scan` and `Web typecheck, test, and build` on pull requests that change nothing but documentation. `Build and test` is untouched, because four of its guards are load-bearing on a markdown-only change. Closes #782 ## The question is inverted, on purpose The gate does not ask "which jobs does this change need". That is a hand-maintained list of what someone thought of, which is the shape `AGENTS.md`'s guard rules tell you to avoid. It asks **"does this pull request contain literally nothing but documentation"**, so a path nobody has classified is code by default and the full suite runs. That also resolves the issue's decision 3, which warned that deriving one shared trigger condition for both jobs would be the easy mistake. It would be, for a *positive* trigger set. For this predicate a single shared condition is correct by construction, because "contains nothing but documentation" is safe for both jobs at once. ## Fail-closed in one direction only A wrong `false` costs four minutes of runner time. A wrong `true` skips the image build and the Trivy scan on a change that needed them. So every failure path answers `false`: - The classify step short-circuits to `docs_only=false` on any event that is not `pull_request`, without consulting git. - It runs without `set -e`, and a git failure, a node failure or an unreadable stdin all land on `docs_only=false` with the step still green. - The jobs test `needs.changes.outputs.docs_only != 'true'`, not `== 'false'`. If the gate produces no output at all, the jobs run. - `!cancelled()` is what lets that condition be evaluated when the gate job itself failed. ## `publish` cannot be starved of an image `publish` declares `needs: [build-and-test, web, image]`, and per #351 a merge that produces no image leaves a release draft that can never be promoted. Two separate protections, because the first one alone was not enough: The gate never fires outside `pull_request`, so a push to `main` runs `web` and `image` exactly as before. And `changes` carries `continue-on-error: true`, because `publish`'s `if:` uses no status function and therefore carries an implicit `success()` over its whole ancestor chain — so a `changes` job that failed for any reason would have skipped `publish` on `main` even though `web` and `image` ran and passed. That hole was introduced by adding `needs: [changes]` and is closed by making the gate unable to fail. The classifier's self-tests therefore run in their own `classifier-self-test` job with no dependents, since a `continue-on-error` job cannot fail a run and a guard that cannot fail a run is not a guard. `ci.yml`'s diff is 134 added lines and **zero removed**. `publish`'s `needs` and `if:` are byte-identical. ## Three corrections found before this was pushed The diff went to an adversarial reviewer first, per the "Writing a guard" rule. It broke the first design in three places, each verified against the repository rather than accepted on argument. **`specs/**` is not documentation here.** `web/src/routes/helpGlossary.test.ts:23` reads `../specs/product/GLOSSARY.md` from disk and fails when a spec term is renamed out from under the in-app glossary (#657). That test runs in the `web` job, one of the two this gate skips, so a specs-only pull request would have skipped the guard written for specs-only pull requests. Carving out that single file would work today and fail silently the first time a second web test reads a second specs path, so the whole tree is code. **`--name-only` hides a rename's source.** `diff.renames` defaults to true, so `git mv src/Cluckwork.Domain/Common/Result.cs docs/Result.cs` arrives as the single path `docs/Result.cs` and classified as documentation while a source file was deleted. Measured on this repository, not reasoned about. `--no-renames` is now in the workflow, an end-to-end test performs that exact `git mv`, and a second test reads `ci.yml` and asserts the flag is on the classifying `git diff`, because the module and the workflow each held a copy of that contract. **The gate reopened the publish hazard**, covered above. ## Verification 16 `node:test` cases, and 13 mutations applied by script, run, and restored with a byte-identical diff check. Every mutation went red and every test went red under at least one. They include `every` to `some` (the "are any docs changed" bug the issue names), dropping the empty-list check, dropping the trailing slash so `docsomething/x.cs` matches `docs/`, letting any `*.md` count as root documentation, and removing `--no-renames` from `ci.yml`. One earlier mutation survived, and the code was fixed rather than the claim: the unreadable-stdin catch was unreachable through an async iterator, so the CLI now reads fd 0 with `readFileSync`. Against real history: PR #768 answers `true`, PR #779 answers `false` (it is the mixed case, carrying `AGENTS.md` and two `docs/` files beside lock files and tooling), and release PR #542 answers `false` because `version.txt` is not root markdown. Six of the seven documentation-only PRs the issue measured answer `true`; the seventh is #718, ten `graphify-out/` files, excluded deliberately. The push path was demonstrated rather than argued: the classify step's `run:` block was extracted from the parsed YAML and executed with the event name forced, and `push` and `workflow_dispatch` both write `docs_only=false` and exit 0 without touching git. ## One question the issue asked that could not be answered It named "establish which status checks are required" as the blocking first step. It could not be read: this repository has no rulesets and classic branch protection returns 403 for a personal access token. It turns out not to matter, and the decision record says why. A job skipped by `if:` reports as *skipped*, which satisfies a required check, whereas a workflow skipped by `on.pull_request.paths` leaves its check in `Expected` forever and blocks the merge. The mechanism chosen here is safe either way, so the unknown is dissolved rather than deferred. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **CI Improvements** - Documentation-only pull requests now skip the web and image CI jobs, reducing validation time. - Changes are classified conservatively; ambiguous or failed detection continues to run the affected checks. - Build and test validation remains enabled for all changes. - **Documentation** - Added a decision record describing documentation-only pull request handling, its scope, and safeguards. - Updated the decisions index with the new record. - **Tests** - Added comprehensive coverage for documentation-change detection and CI behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: mforce <cleyva@clvc.net>
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.
Closes #767
AGENTS.md's #394 rule and its decision record both state thattools/simulation/k6/and thePlaywright specs in
tools/simulation/ui/are uncovered by CI. Half of that stopped being true on2026-08-08, and it is the half a reader is most likely to act on.
The evidence
.github/workflows/e2e-smoke.ymlrunson: pull_request, path-filtered tosrc/**,web/**,tools/simulation/**,deploy/**, the three root image-build inputs and the workflow itself. Its ownheader dates the change:
A write-contract change lives in
src/by definition, so the Playwright specs run for exactly theclass of change this rule governs. Roughly 28 specs across 14 files, about three minutes. Only
slow(the real 15-minute token-expiry spec) and
canarystay dispatch-only, and a docs-only PR skips the job..github/workflows/k6-baseline.ymlis stillworkflow_dispatch:only, so k6 is genuinelyuncovered, exactly as the rule says. Everything about its tolerated-status list letting a broken
write pass against a 100% green baseline is unchanged, and it is the sharper trap of the two.
Observed rather than inferred: on #766 the check "Playwright smoke over the simulation fixture" ran
and passed, and that run included
worker-sale-allocation.spec.ts, which carries nosloworcanarytag. The decision record was the more specific and more wrong of the two — it said the specs were
"
workflow_dispatchonly, #385".Why it is worth correcting
The error points the safe way: it makes you verify by hand something CI already covers, so nothing
has shipped broken because of it. The cost is misdirected effort. On #727 the Playwright half of the
#394 caller read was done by hand under the belief that a green baseline would hide a break, when CI
would have caught it. It also flattens the one caller that genuinely still needs reading into a list of
two, which makes it easier to skip.
It is a trap in the other direction too. Anyone who checks the workflow, sees Playwright is covered and
concludes the whole bullet is stale will stop reading the k6 callers as well.
What changed
AGENTS.md— the bullet now separates the two, names the mechanism rather than the state (since thestate has moved once already), and keeps the rule's durable half: verify by reading, because CI can
tell you a spec broke but not that a caller's intent changed.
docs/decisions/394-write-contract-callers.md— a dated amendment rather than a rewrite. What therecord said was true when it was written, and a decision record that quietly reflects later facts stops
being a record. The original paragraph keeps its one factual correction (
k6-baseline.ymlnamedexplicitly, the false Playwright clause removed) and the amendment explains what moved and when.
Not in scope
Whether the path filter should also cover a docs-only PR that changes a spec's expectations. It should
not, but that is a separate argument from this correction.