Conversation
Stop for breath mid-sentence and the transcriber writes a period, then capitalises the next word. "you should see a parrot at the top right of your screen" comes out as two sentences. `SentenceProbe` has answered this question since #227 and nothing called it. This is the consumer. After the vocabulary pass, every `word. Capital` boundary in an English transcript is scored, and two thresholds decide: below -4 the period is removed silently, below -2 the join is recorded and not written, above that nothing happens. Both are `transcription.sentences` in the config, and `sentences: false` turns the stage off. Joining lowercases the word after the period, unless there is a reason not to: `I`, a word in capitals throughout, a name NLTagger recognises, a word NLTagger gives no lemma for, or a term in vocabulary.yaml. English only, and only where the sentence model is already on disk. Nothing is downloaded and nothing waits. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NMVgtb4jmhQtvWtSJr1bce Signed-off-by: Nathan Zylbersztejn <nathan.zylbersztejn@gmail.com>
Signed-off-by: Nathan Zylbersztejn <nathan.zylbersztejn@gmail.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`check-slot-gate.sh` and `check-sentence-join.sh` were hand-run because they need the 300 MB ModernBERT model. They are the only defence of the claim that the app does not rewrite text when nothing asked it to, so out of CI they drift silently. CI now fetches the model with `--sentence-model` after the build and caches it at `~/Library/Application Support/ParrotFlow/models`, keyed on `SentenceModel.swift`. Both scripts are added to `checks.yml` and to `CHECKS` in the Makefile, and `make test` fetches the model too. `check-slot-gate.sh` passed vacuously without a model: every case routed to `judge`, so nothing was applied, nothing was declined, both wrong counters were zero and the script exited 0. It now fails when `applied + declined` is zero. Signed-off-by: Nathan Zylbersztejn <nathan.zylbersztejn@gmail.com>
|
| # quoted only so nobody reads it as two paths. | ||
| path: "~/Library/Application Support/ParrotFlow/models" | ||
| key: sentence-model-${{ runner.os }}-${{ hashFiles('Sources/ParrotFlow/SentenceModel.swift') }} | ||
| restore-keys: sentence-model-${{ runner.os }}- |
There was a problem hiding this comment.
Prefix-restored model cache bypasses freshness checks
The sentence-model-${{ runner.os }}- restore prefix can recover an older model cache after the exact key changes. The following --sentence-model invocation accepts a cache containing locally loadable expected files and returns it before downloading; it does not compare an upstream revision, manifest, or content hash. The slot-gate and sentence-join checks can therefore score stale model weights while appearing to use the current model configuration. Pin the model revision, validate a persisted manifest or content hashes before reuse, and include that immutable revision in the cache key.
Artifacts
Executable stale sentence-cache decision check
- This authored narrow check asserts the production cache predicate and cached-load control flow, then exercises empty and older-cache scenarios; the takeaway is that the exact decision path is testable without the unavailable macOS CoreML runtime.
Empty sentence-model cache takes fetch path
- The executed before scenario reports `isCached=False` and a fetch decision; the takeaway is that the control checks the cache predicate before fetching.
Older loadable sentence-model cache is accepted
- The executed after scenario marks the cache `older-upstream-revision`, reports `isCached=True`, and takes `load cached copy; do not call fetch`; the takeaway is that stale loadable weights bypass freshness validation.
Native Swift runtime unavailable in Linux validation runner
- The attempted `swift --version && swift build -c release` command exits 127 because `swift` is not installed; the takeaway is that a macOS/CoreML end-to-end binary run was blocked by the environment.
| # AppVariant.displayName, which reads the bundle identifier, and a bare | ||
| # SwiftPM binary has none. | ||
| - name: Cache the sentence model | ||
| uses: actions/cache@v4 |
There was a problem hiding this comment.
Cache action is selected through a mutable tag
actions/cache@v4 selects external executable CI code by a movable tag rather than an immutable commit. If that tag is moved upstream, the same workflow revision can execute replacement code without a repository change. Pin the action to the reviewed full 40-character commit SHA for the intended release and update that SHA through the normal dependency-update process.
Artifacts
Targeted immutable action-reference checker source
- Captured the authored checker that reads workflow line 86, validates the reference format, and executes a local tag-move reproduction, with the takeaway that the check directly exercises the reported CI integrity condition.
CI action tag before a tag move
- Executed the checker before moving a local v4 tag; it found `actions/cache@v4` at line 86 and resolved the tag to trusted action content, with the takeaway that the workflow uses a non-SHA tag.
CI action tag after a forced tag move
- Executed the same checker after force-moving the unchanged local v4 tag; it resolved to replacement action content, with the takeaway is that a tag move can change CI code without changing the workflow.
Two scored sets were hand-run because they need the 300 MB ModernBERT model:
scripts/check-slot-gate.shandscripts/check-sentence-join.sh. They are theonly defence of the claim that the app does not rewrite text when nothing asked
it to — which name proposals get written unasked, and which periods get removed
unasked. Out of CI they drift silently. CI now fetches the model and runs both.
make testfetches it too. On a machine that has never run it that is a one-off40s download plus a 7s Core ML compile; after that the run is a few seconds
slower and nothing else. This is stated here rather than hidden:
make testnowtouches the network on a fresh clone.
Review first: the cache key on
checks.yml, and the new guard at the bottom ofcheck-slot-gate.sh.Based on #230. The diff against
mainwill shrink once that merges.check-slot-gate.sh passed with the model never running — the whole point of it
The script ended with:
With no cached model every case routes to
judge. Nothing is applied, nothingis declined, both counters are zero, and the script exits 0. Confirmed by
moving the cache aside:
A human reading
applied 0 declined 0off the terminal sees it. CI does not.A failed fetch or an empty cache restore would have produced a green tick over
nothing.
The fix: fail when
applied + declinedis zero. The other option wasfailing when
judgeequals the scored total. Both fire on exactly the sameruns today.
applied + declined == 0states the invariant directly — the gatesettled nothing — and it does not need to know how many cases were scored, so
it survives a change to the case set.
check-sentence-join.sh needs no second guard — it already fails first
Confirmed the same way, cache moved aside:
It fails earlier than expected. The stored per-case score would catch a model
that never ran —
driftedcompares each case against the number the probemeasured when the set was built, and a tier count with no scores behind it
cannot match. But
SentenceJoinCommandguards onSentenceModel.isCachedandprints no boundary block at all, so
reading()returns nothing and the runstops on the first case. Two ways to fail, both before any number is reported.
No guard added.
The numbers CI produces, and why slot-gate is 15/14/21 and not 13/16/21
Measured on this branch, Apple M-series:
15/14/21 and not 13/16/21. The split moved in #229, which added the sentence to
the slot decision;
SlotGate.swift:46anddocs/transcription.mdalready record15/14/21. The judge count and the settled total are the same either way, and
both error counts are still zero.
Neither script asserts a split.
check-slot-gate.shasserts zero wrong appliesand zero wrong declines, plus the new "settled something".
check-sentence-join.shasserts zero false joins and zero drift. That matters on a runner: a small shift
in the split is not a failure, but a wrong apply is.
Run time — 4 min today, and where the new work lands against the 20 min limit
Measured locally, warm:
--sentence-model, cached--sentence-model, coldcheck-slot-gate.shcheck-sentence-join.shSo about 36s warm and 78s cold on top of the current ~4 min run.
make testwent from about 1:20 to 1:53 on a warm build.
The real CI numbers are in the checks on this PR — the first run is the cold
one, any push after it is warm.
timeout-minutesstays at 20. I have notraised it, and if the cold run comes anywhere near it that is worth saying out
loud rather than papering over.
The warm fetch is 5.3s, not the ~0.1s a no-op would cost. It loads the compiled
model into Core ML as well as checking the cache, which is the same call the
first English dictation makes.
What the cache is keyed on, and why not a revision sha
SentenceModel.swiftholds the repository, the file list and the package name.Change any of them and the cached copy is the wrong one.
HubDownloadpins to the HuggingFace repository'smain(
HubDownload.swift:36), not to a revision sha, so there is no upstreamrevision to key on. The
restore-keysfallback takes the last run's copy.The path is
ParrotFlowand notParrotFlow Dev.AppVariant.displayNamereads the bundle identifier and a bare SwiftPM binary has none, so
AppVariant.isDevis false.Comments that said the opposite, and one list that was already out of step
Updated, because a comment claiming a check does not run is worse than none:
scripts/check-slot-gate.shheader — "Not inmake testand not in CI".scripts/check-sentence-join.shheader — the same, plus the claim that amissing model just makes the run "say so".
.github/workflows/checks.ymltop block — "without a model" became "withoutan LLM", and "Four of the nine
scripts/check-*.sh" lost its count. Thereare 41 of them now, so the number was long stale.
sentence-casestep comment on the base branch, which says the scoredhalf "is run by hand".
Makefile— theCHECKScomment.CONTRIBUTING.md,docs/cli.md(three places),docs/transcription.md.Not fixed here:
CHECKSandchecks.ymlwere already out of step beforethis PR.
keyedis in the Makefile and not in CI;bug-reportis in CI and notin the Makefile. Both predate this change and neither is about the sentence
model, so they are left alone rather than folded into a CI change.