Skip to content

chore(op,lexicons): the search-attribute and workflow vocabulary, safeHeartbeat and its forty callers - #2203

Merged
lex00 merged 2 commits into
mainfrom
chore/2195-vocabulary-sweep-dead-code
Sep 7, 2026
Merged

chore(op,lexicons): the search-attribute and workflow vocabulary, safeHeartbeat and its forty callers#2203
lex00 merged 2 commits into
mainfrom
chore/2195-vocabulary-sweep-dead-code

Conversation

@lex00

@lex00 lex00 commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Closes #2195. Sub-issue 4 of #2191, the last one.

Two commits. The first rewrites every comment, docstring, test name and fixture the audit listed, against what the local executor actually does. The second deletes safeHeartbeat and everything that only existed to call it, points FTN020 at core's cron validator, and takes --temporal out of the four CI pipeline test fixtures.

The vocabulary half is mostly one substitution repeated: what these comments call a "search attribute" is an outcomeAttribute on a step, which the executor captures onto that step's StepRecord and the runtime folds onto the run ledger record (packages/core/src/op/local-executor.ts:278-284, packages/core/src/op/runtime.ts:90-98). It is a run outcome, not a queryable index on a live workflow. "Workflow", where it meant the Op or the run, is the Op or the run. OpConfig.name names dist/ops/<name>/; the workflow function name it also claimed to name went away with 257ca0c. signalName is left alone here and gets its own issue, #2202.

The dead-code half removes a real cost, not only a dead symbol: five of the safeHeartbeat call sites sat inside a 15 second setInterval that ran for the whole length of a shell-out and did nothing on each tick.

Three items on the list were already fixed by the sibling PRs and are not touched: the thrown message in lexicons/k8s/src/composites/operator-stack.ts (#2197 rewrote it to cite OPS014 and the unattended-approval reason), and the heartbeat and "worker loads it" comments in the activity files (#2198). Their doc comments still said TMP014, so those are rewritten here.

Acceptance

  • Every listed line is rewritten against the current mechanism, and the PR body lists each with its replacement. The table below. git show ca00f1a7 --stat covers it.
  • safeHeartbeat does not exist and no setInterval heartbeat loop remains in any activity. rg -n --text "safeHeartbeat" packages lexicons is empty (exit 1). rg -n "heartbeat" -i packages lexicons -g '!**/generated/**' leaves only fountain's SSE keep-alives, the heartbeatTimeout removal notes in activity-profiles.ts, and the lease doc.
  • FTN020 imports core's cron validator and its doc records the @daily decision. lexicons/fountain/src/lint/post-synth/ftn020-schedule-cron-syntax.ts:2 imports isValidCronExpression from @intentius/chant/op; lines 19-26 record the decision, as do the audit-catalog guidance and lexicons/fountain/docs/pages/resources.mdx:59.
  • No test fixture uses --temporal as a run command. rg -n -- "--temporal" lexicons is empty. The four remaining hits repo-wide are the bridge in packages/core/src/cli/main.ts, its test, and the two docs pages that document it.
  • The typecheck baseline does not grow, the full suite passes, depcheck and check-lexicons pass. See Gates.
  • A follow-up issue exists for renaming signalName. rename signalName on gate() and GateStep to a name that does not imply a signal #2202.

The @daily decision

FTN020 used to accept the @nickname shorthands; core's validator never has. Rejecting them, per the issue's recommendation: chant has one idea of what a cron string is, and cronMatches/cronDueBetween cannot evaluate a nickname, so a cadence written that way is one chant operator would never fire even though fountain upstream would store it. Recorded in the rule's module doc, its description, the audit-catalog guidance (and so the generated audit-rules.mdx row), and the fountain resources page. The test that asserted @daily passes now asserts it and four siblings are refused.

Every rewritten line

"Search attribute" for what is now a run outcome

File Was Is
packages/core/src/op/local-executor.ts:50,55 "The first search attribute the step published" / "Every search attribute the step published" "The first outcome attribute…" / "Every outcome attribute…"
packages/core/src/op/activities/lifecycle.ts:64,70 "inside watch/observation workflows" / "surface drift as a workflow search attribute" "inside watch/observation Ops" / "surface drift as the run's Drift outcome"
packages/core/src/op/composites/watch-op.ts:5-6 "The Op codegen (#7) emits a workflow that runs phases sequentially" + "The auto-emit search-attribute behavior (#28) tags each phase" "The local executor (../local-executor.ts) runs the phases in order and files each step's record under the phase it belongs to"
packages/core/src/op/composites/watch-op.ts:80,89 "workflow-level Drift search attribute, making 'show me runs that detected drift' a one-filter UI query" / "a StaleReceipts search attribute" "the run's Drift outcome on the run ledger, so a reader of the ledger can pick out the runs that detected drift" / "a StaleReceipts run outcome"
packages/core/src/op/composites/workflow-audit-op.ts:78 "a workflow-level search attribute so 'show me audits that found drift' is a one-filter UI query" "the run's Findings outcome on the run ledger, so a reader can pick out the audits that found something"
packages/core/src/op/composites/lexicon-upgrade-op.ts:124-125 "a workflow-level search attribute so 'show me lexicons with an upgrade ready' is a one-filter query" "the run's HasUpgrade outcome on the run ledger, so a reader can pick out the lexicons with an upgrade ready"
packages/core/src/op/composites/composites.test.ts:71,133,354 and :75,128,190 "a workflow search attribute" / "auto-emit search attrs include Watch + Env" "a run outcome" / "labels include Watch + Env" (they are labels)
packages/core/src/op/composites/lexicon-upgrade-op.test.ts:45 "an outcome search attribute" "a run outcome attribute"
packages/core/src/lint/rules/op/ops014-converge-rule-refusals.test.ts:244 "no Converge search attribute" "no Converge label"
lexicons/cedar/src/dogwood/replay-op.ts:212 "a workflow search attribute, so 'show me the replays that found something' is one filter" "the run's Divergences outcome on the run ledger, so the replays that found something are readable from the ledger"
lexicons/cedar/src/dogwood/replay-activity.ts:97 "the number a search attribute or a gate reads" "the number an outcome attribute or a gate reads"
lexicons/cedar/src/dogwood/replay-activity.test.ts:413 "rides out as a search attribute" "rides out as a run outcome"

Adjacent, same error, found by the grep the issue asks the PR to run — the terraform composites, which #2198 did not reach: terraform-watch-op.ts:10,43,129, terraform-watch-op.test.ts:48, terraform-apply-op.ts:92,216, terraform-apply-op.test.ts:103, terraform-apply-op.acceptance.test.ts:133, terraform-adopt-op.ts:186, terraform/src/op/activities/terraform.ts:378, terraform/src/op/adoption.ts:22, terraform/src/skills/chant-terraform.md:68. All become run outcome / outcome attribute. Two of them are gate description strings a human reads, so the wording matters there.

"Workflow" for the Op or the run

File Was Is
packages/core/src/op/activities/converge.ts:2,60 "a ConvergeOp-generated workflow's Converge phase" / "bakes into the workflow's convergeTick step args" "a ConvergeOp-generated Op's Converge phase" / "bakes into the Op's convergeTick step args"
packages/core/src/op/activities/env-teardown.ts:49,53 "Default: the worker's cwd" / "reports back to the workflow" "Default: the chant process's cwd" / "reports back to the run"
packages/core/src/op/activities/env-teardown.ts:152-153 "the workflow decides what a failed teardown means for the run" "A failed step is what the executor aborts the phase on and what the Op's onFailure compensation runs for."
packages/core/src/op/activities/apply.ts:93 "what a workflow can gate on regardless of target" "the shape every Op reads back regardless of target"
packages/core/src/op/builders.ts:366 "default the worker's cwd" "default the chant process's cwd"
packages/core/src/op/builders.ts:868 policyGate "blocking the workflow on any violation" "failing the step on any violation so no later phase runs" (it throws)
packages/core/src/op/step-output-ref.ts:14-16 "a value placeholder the build resolves and the serializer compiles into a real local variable in the generated workflow" "a value placeholder that stays inert in the Op's data until the executor substitutes the producing step's recorded result for it, just before the consuming activity is called (resolveStepOutputRefs over resultsById)"
packages/core/src/components/cli-support.ts:435 "matching what the generated orchestrator Op will eventually do — the same order chant graph --components reports" "in the same order chant graph --components reports"
packages/core/src/op/types.ts:12 "Used as the workflow function name (camelCase) and output directory name" "Names the Op's output directory (dist/ops/<name>/), and is the name chant run <name> and another Op's depends refer to"
watch-op.ts:32, converge-op.ts:100, reconcile-op.ts:36, pipeline-audit-op.ts:33, workflow-audit-op.ts:37, lexicon-upgrade-op.ts:63 "Also the generated workflow function name, camelCased" "Names the Op's output directory and is what chant run takes"
packages/core/src/cli/commands/build.test.ts:952-983 test "op worker files go to dist/ops", fixtures ops/durable-hello/{workflow,worker,activities}.ts test "serializer files under ops/ go to /dist/ops even with no --output", fixtures ops/nightly-report/{steps.json,notes.md,nested/extra.txt} — the routing is on the ops/ prefix alone, so the test now proves that. build.ts:766 and its two messages follow.

Adjacent, same class, all in packages/core/src: types.ts:169 ("carried into the generated workflow as a comment" → "into the Op's build output"), op-ir.ts:61 ("resolved by the serializer into a local variable in the generated workflow.ts" → "which the executor substitutes for at run time from the producing step's record"), apply.ts:451, receipt-store.ts:111, activities/reconcile.ts:25, apply-op.ts:74 ("not by a signal to a running workflow" → "which the next run reads; nothing waits in the meantime"), component.schema.json's Gate.signalName description ("Signal name the generated workflow waits for" → "The gate's name — what chant approve <component> <gate> resolves"), and the six /** Op resource — generates the X workflow */ lines on the composites.

Other

File Was Is
packages/core/src/components/auto-release.ts:16-17 "the kind of thing a replay-safe hosted runtime must not do inline either" "the kind of thing that belongs at the edge of a run rather than inside a step a retry may run a second time"
packages/core/src/op/composites/converge-op.ts:74,75,83 TMP014 OPS014
lexicons/k8s/src/composites/operator-stack.ts:40,44,49,107,201 TMP014 OPS014; :49 also drops "(the local dispatch executor can't honor its required gate)" for OPS014's own unattended-approval reason, matching the message #2197 wrote; :33 drops "independently of the temporal lexicon" for "from the Op configs alone"
packages/core/src/op/activities/converge.ts:73,76,205,216,232,254,375, converge.test.ts:195, op-verb-class.ts:118, lexicons/k8s/src/composites/composites.test.ts:3871,3883 TMP014 OPS014 (same error, adjacent)
packages/core/src/op/composites/converge-op.ts:176,178 TMP012 / TMP013 OPS012 / OPS013
packages/core/src/op/cron.ts:37, types.ts:47, cron.test.ts:14,35, op.test.ts:56, converge-op.test.ts:86, composites.test.ts:53 TMP010 cited as a live rule the shared validator in ./cron.ts / cronSyntaxMessage's wording / "a lexicon's own post-synth cron check"
lexicons/{aws,azure,gcp,k3d,k3s}/src/op/builders.ts:4-6 "rather than in core or the temporal barrel… wiring them into the cloud-agnostic temporal barrel would make it depend on every cloud lexicon at runtime" "rather than in core: core cannot import a lexicon's types, so a wrapper whose opts IS the activity's own *Args interface can only sit in the lexicon that declares that interface"
lexicons/k8s/src/op/builders.ts:25 "which is exactly the 'a worker image needs no kubectl binary' / 'core stays product-agnostic' property" "so a project that never touches Kubernetes would install both just to import anything from op. That is exactly the 'core stays product-agnostic' property…"
lexicons/azure/src/deep-observe.ts:342 "the way AWS's or temporal's readers do" "the way AWS's own reader does"
lexicons/aws/src/codegen/docs.ts:117 "the overview/reference pairing temporal uses" "a hand-written overview beside a generated reference"
lexicons/fly/src/components/run-agent-conformance.test.ts:22-26,174-182 "the durable-identity channel… On a durable runtime, run() and rollback() execute as separate steps" "the rollback identity channel… A runtime that splits a run across process boundaries — a generated CI pipeline running each component as its own job, say — rebuilds input from JSON for every call". This is the case Capability.rollback's own doc already describes, and the sprite fixture name follows.
vitest.config.ts:111-114 "The Temporal runtime/compile-smoke suites bundle workflows with webpack in-process" "The heaviest tests here do real work on disk — build.test.ts's discovery, the CLI end-to-end cases — and sharing a machine with the rest of the fork pool they have been seen past 5s while still making progress." No measurement claimed; the 20s value is unchanged.
test/no-egress.test.ts:274-277 "on a saturated run (the fourteen-fork pool, the temporal suites bundling workflows with webpack in-process)" "sharing a machine with the rest of the fork pool"
test/forgejo-preview-e2e.sh:13, test/forgejo-preview-e2e/README.md:21 "packs @intentius/chant + the fly and temporal lexicons" / "packs core + fly + temporal" "packs @intentius/chant and the fly lexicon" / "packs core + fly" — the two -w flags at lines 98-100
packages/core/src/components/pilots/pilots-lint-clean.test.ts:62-63 "acknowledge it with // chant-disable-next-line COMP004 -- <reason>" "acknowledge it with a file-level // chant-disable COMP004 -- <reason> … a COMP* diagnostic is reported for the whole component and carries no line, so -line/-next-line do not suppress one"
.../__fixtures__/comp/comp004/pass/orders-table.component.ts:4 "nothing requires the durable backend" "nothing here stops the run for a person"
docs/src/content/docs/lint-rules/composition.mdx:140 heading "COMP004: Gate Needs Durable Runtime" heading "COMP004: Gate Ends The Run Pending Approval", the rule's own description, with a paragraph saying the id and the file name deliberately stay (renaming either churns every chant-disable comment and every import). The same note is in the rule file's module doc.

Dead code

  • safeHeartbeat and its export are gone (packages/core/src/op/activity-runtime.ts, op/index.ts). 44 call sites removed across core's emulator-lifecycle.ts and aws, azure, fly, gcp, gitlab, helm, k3d, k3s, k8s, render and terraform. The issue did not list render; rg skips render-apply.ts as binary (it contains a NUL byte), so --text was needed to find its four.
  • Five live setInterval loops, not four: helm's two install paths (helm.ts:434,565), k3s.ts:142, k3d.ts:160, and kubectl.ts:312 (whose interval body was the single safeHeartbeat call). Plus terraform's withHeartbeat wrapper (terraform.ts:736), which ran the same 15s timer around seven calls; its seven call sites are unwrapped and the helper, HEARTBEAT_MS and the clearInterval plumbing deleted. helm's pinned path keeps its try/finally because the finally also removes the temp wrapper chart.
  • lexicons/fountain/src/lint/post-synth/ftn020-schedule-cron-syntax.ts imports isValidCronExpression from @intentius/chant/op instead of inlining a copy; the @daily decision is above.
  • --temporal as a sample runCommand in lexicons/{github,gitlab}/src/components/generate-{op-,}pipeline.test.ts becomes --json (the Op pipelines) and --verbose (the component pipelines). Both are real chant run flags, so the fixture teaches something that works.
  • packages/core/src/cli/handlers/run.test.ts:82 makeOp drops taskQueue?: string.

Grep survivors

rg -n --text "safeHeartbeat" packages lexicons — empty.

rg -n --text "search attribute|searchAttribute" packages/core/src lexicons/*/src — 8 lines, all deliberate:

Line Why it stays
lifecycle/run-ledger.ts:8, op/types.ts:28, op/runtime.ts:94, op/op-ir.ts:104 Each names the deleted OpConfig.searchAttributes field by its own name to say it is gone and what replaced it ("used to carry", "are gone"). Renaming the field in its own obituary would make the sentence false.
op/op-ir.test.ts:65,73,79 The IR format-2.0 test asserts searchAttributes is absent from the emitted JSON. The string is the assertion.
op/step-output-ref.ts:3 A verbatim quote of issue #1290's title.

rg -n --text "TMP01" packages lexicons -g '!**/generated/**' — 15 lines:

Line Why it stays
lint/rules/op/{index.ts:3,support.ts:6,ops012*.ts:4,ops013*.ts:6,ops014*.ts:3} and their three test headers Provenance: "ported from"/"moved from a hosting lexicon's own TMP012/TMP013/TMP014". #2195 lists provenance notes as out of scope.
cli/commands/build.ts:508, cli/commands/lint.ts:429 Same provenance sentence, naming where the OPS0xx checks came from.
lint/post-synth.test.ts:137 Named as out of scope by the issue ("shaped like temporal's TMP012/TMP013").
op/cron.test.ts:2 "the validator ported from TMP010" — provenance.
lexicons/k8s/src/composites/composites.test.ts:3990 .not.toThrow(/TMP014/) is #2197's own regression assertion that the message no longer cites the retired rule. Changing it would delete the guard.

One more that a @daily grep turns up and stays: lexicons/fountain/src/generated/index.d.ts:60 says the nicknames work. That file is generated from fountain's OpenAPI spec and describes the server, which does accept them; FTN020 is chant being stricter, which is what the rule doc now says.

Gates

Run from the worktree root at 71fe1914.

Gate Result
npx vitest run packages/core lexicons/{fountain,k8s,helm,k3d,k3s,github,gitlab,aws,azure,gcp,fly,terraform,cedar,render} 1122 files passed, 3 skipped; 15076 tests passed, 30 skipped
npx vitest run packages lexicons (alone) 1171 files passed, 3 skipped; 15677 passed, 30 skipped. 5 failures in packages/core/src/cli/commands/init-lexicon.test.ts and packages/core/src/cli/security-boundary.test.ts, every one a 20s Test timed out, no assertion failures. Re-run alone: 2 passed (2) / 31 passed (31) in 13.5s. The known CLI end-to-end flake under pool load.
npx vitest run packages/core (after the last edits) 385 files passed, 6161 tests passed
npx tsc --noEmit -p packages/core/tsconfig.json clean
npx tsc --noEmit clean
npx tsx scripts/typecheck.ts "37 known failure(s), no regressions. (37 baselined)"
npx tsx scripts/check-lexicons.ts "No untracked tier-1 failures." 23/23 tier-1 for every lexicon
node scripts/depcheck.mjs "no undeclared runtime dependencies"
npx eslint packages/ 0 errors, 2 pre-existing no-explicit-any warnings in meta/publish-metadata.test.ts
node scripts/lint-docs-sentences.mjs "4108 gated finding(s), no regressions — 3 BELOW the baseline of 4111". Exit 0. Baseline left where it is rather than ratcheted, so this PR carries no baseline churn
node scripts/check-docs-diataxis.mjs "21 tutorial, 52 how-to, 58 reference, 34 explanation", exit 0

docs/src/content/docs/lint-rules/audit-rules.mdx was regenerated with npx tsx scripts/generate-audit-rules-doc.ts; the diff is the two FTN020 lines and nothing else. The fountain rules.mdx and resources.mdx twins were edited by hand to match their sources, rather than regenerated, since the local manifests are stale (#2199).

Follow-up

#2202 — rename signalName on gate() and GateStep, with a deprecation, listing the authored surfaces that carry it.

🤖 Generated with Claude Code

https://claude.ai/code/session_0148a4dPTErCBpmNy7J7933f

lex00 and others added 2 commits September 6, 2026 23:25
…ism, not the engine that is gone

Sub-issue 4 of #2191, the vocabulary half. Every line here named a mechanism
chant no longer has, so each one is rewritten against what the local executor
actually does.

"Search attribute" becomes the run outcome or the outcome attribute: an
`outcomeAttribute` on a step is captured onto that step's `StepRecord` and
folded onto the run ledger record, which is what a reader filters runs by.
"Workflow", where it meant the Op or the run, becomes the Op or the run.
`OpConfig.name` names the Op's output directory; the workflow function name it
also claimed to name was deleted in 257ca0c. "The worker's cwd" becomes the
chant process's cwd, and the one "replay-safe" argument becomes the reason it
was standing in for. TMP014 becomes OPS014, which is the rule that ships;
TMP010 as a live peer becomes the shared validator in `op/cron.ts`.

The six lexicon step-builder headers stop contrasting themselves with a
temporal barrel and say why the builders live in the lexicon: core cannot
import a lexicon's `*Args` types. The vitest and no-egress timeout comments
drop the webpack-bundling claim. `forgejo-preview-e2e` says the two packages it
actually packs. The COMP004 docs heading becomes the rule's own description,
with a note that the id and the file name deliberately stay.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0148a4dPTErCBpmNy7J7933f
…rts core's cron validator

Sub-issue 4 of #2191, the dead-code half.

`safeHeartbeat` was a no-op kept alive so the lexicons calling it would keep
compiling. Forty-four call sites across core, aws, azure, fly, gcp, gitlab,
helm, k3d, k3s, k8s, render and terraform are gone, and so is the export. Five
of them ran a live 15 second `setInterval` for the length of a shell-out
(helm's two install paths, k3s install, k3d cluster create, and terraform's
`withHeartbeat` wrapper around seven calls); the timers and their
`clearInterval` plumbing go with them, since nothing was listening and nothing
acted on a missed tick.

FTN020 now imports core's `isValidCronExpression` rather than carrying a
divergent copy of it, and refuses the `@daily`-style nicknames the copy used to
accept. chant validates and matches one cron notion: `cronMatches` cannot
evaluate a nickname, so a cadence written that way is one chant itself would
never fire. The rule doc, its audit-catalog guidance, the fountain resources
page and the test record the decision.

The four CI lexicon pipeline tests stop teaching `--temporal` as a sample
`runCommand` (it exits 2) and use `--json` or `--verbose`, and `run.test.ts`'s
`makeOp` drops the `taskQueue?` field `OpConfig` has not had since #2118.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0148a4dPTErCBpmNy7J7933f
@lex00
lex00 merged commit 7255abb into main Sep 7, 2026
7 checks passed
lex00 added a commit that referenced this pull request Sep 7, 2026
…2214)

Peer session dropped findings in #2198, #2203, #2206; this session dropped
findings in #2208, #2210, #2213. Each PR deferred the ratchet so the drops
would not conflict on the same baseline file, per PR #2174's precedent that
the baseline is ratcheted down rather than left slack.


Claude-Session: https://claude.ai/code/session_01FoXyD9UvKQ5ZdhiR9JT1yB

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
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.

sweep: search-attribute and workflow vocabulary, safeHeartbeat and its forty callers, and the other dead code #2116 left

1 participant