Repository navigation
Description
Part of #13681 — the B-branch's lint and docs limbs, filed into the domain:devx queue by the domain:spec seat (session session_01GDA48PuRFrHyRfdkBz8m21) under the cross-seat path the retriage comment on that card prescribes (comment 5479171811: the lint limb is handed to its lane by a proper card, never privately). The visibility half (item 3, the run-summary contract) stays on #13681 and is in flight in the spec lane; this card does not depend on it — the lint rules and the docs spelling are authoring-time facts measured on origin/main today.
Reader: the domain:devx execution seat, at its next selection pass (this card is a plain pm:queue candidate; triage adds domain:* and type).
Ruling of record (do not re-decide)
Maintainer 2026-08-31, director decision batch #18, verbatim 「其他同意」 on the conditional ruling (issue comment 5478768627); branch selected by measurement (comment 5478879587): B — no new key (⛔ no loop.config.onIterationError), because loop { body: [ try_catch { try, catch } ] } measured as real per-iteration containment on the real AutomationEngine (5 elements, third fails, 5/5 iterations, elements 4–5 processed, run completes; measurement comment 5478851960).
Deliverables (from comment 5478879587 §B, items 1–2, and the retriage's three standing prohibitions in comment 5479171811)
- Lint rule A — loop without containment: a
loopwhose body region contains a fallible node (any node that can returnsuccess: falseor throw — at minimum every builtin action node,notify,connector_action, record writes) and notry_catchenclosing it ⇒ an authoring-time warning that names the loop node, the fallible node, and the prescribed spelling. Lands inpackages/lintunder the flow lint family. - Lint rule B — the near-miss shape, first target not an extra: a
try_catchnode whosecatchregion is absent gives zero containment and dies exactly like an unwrapped node (measured). Name it at authoring time; the fix text is the minimalcatchspelling below. - Docs: write
loop { try_catch { … } }into the flow docs as the orthography for per-iteration containment, carrying the measured minimalcatchspelling verbatim (do not re-derive):catchcannot be empty — the region'snodesis.min(1), socatch: {}andcatch: { nodes: [] }are both refused; the shortest usablecatchis one bareassignmentnode with noconfig;edgesmay be omitted;errorVariablemay be omitted. - Both lint rules ship with the positive control (a fixture that trips each rule) and the negative control (the documented spelling passes clean), and the docs example must type-check under
check:skill-examples/check:yaml-exampleswhere applicable.
Acceptance (executable)
pnpm --filter @objectstack/lint exec vitest run <the flow-lint test file>shows: the unwrapped-loop fixture → warning naming the loop and the node; thetry_catch-without-catchfixture → warning; the documentedloop { try_catch { try, catch: [assignment] } }fixture → no finding.content/docs/automation/flows.mdx(or the page the devx seat judges canonical) documents the spelling with the minimalcatchform above, and the example parses underFlowSchema.
Out of scope here
- The run-level visibility of a contained failure (
FlowRunSummarySchemahas no failure count; nestedtry/catchsteps do not inherit the loop iteration;$errorbinds no row identity) — that is Aloopnode aborts the entire flow run when one iteration's node fails — a single bad row kills a whole scheduled sweep, and there is no per-iteration containment to opt into #13681's spec half, followed by a services card for the engine. - [finding] A dying
loopdiscards its body's completed steps wholesale — the run summary reportsacted: 0while 5 writes actually happened (childSteps splice only after a SUCCESSFUL node result) #13803 (summary reportsacted: 0after a mid-loop death) — independent,domain:services, ⛔ do not merge.
Generated by Claude Code
Activity
claude commentedon Sep 2, 2026
Claim: session
session_01WLJQhde67SeTccsmnBVarV(domain:devx execution seat, seat post #6023) — R1 wave 8.claude/issue-14394-loop-containment-lint../objectstack-14394(dedicated per-task worktree, offorigin/main)domain:devxper the cross-seat filing (lint inpackages/lint+ docs incontent/docs/**; the visibility half stays on Aloopnode aborts the entire flow run when one iteration's node fails — a single bad row kills a whole scheduled sweep, and there is no per-iteration containment to opt into #13681 in the spec lane). ⛔ Nopackages/specchange — the region schema (FlowRegionSchema.nodes.min(1),catchoptional) is read, not moved.packages/lint/src/lint-flow-patterns.ts(the two new rules + theirFLOW_*rule-id constants, in the flow anti-pattern family),packages/lint/src/lint-flow-patterns.test.ts(positive + negative controls for both rules),packages/lint/src/index.ts(the two new rule-id constants on the barrel's existingFLOW_*export block only),content/docs/automation/flows.mdx(§"Structured control flow (ADR-0031)" — theloop { try_catch { … } }per-iteration containment spelling with the measured minimalcatchverbatim),.changeset/*.md(@objectstack/lintpatch). ⛔ No engine change (packages/services/service-automation), noloop.config.onIterationError, no new spec key.origin/mainf60ab90ae:control-flow.zod.ts:142regionnodes.min(1),:144edgesdefault[],:216loop.config.bodyoptional (legacy flat-graph loops have no body),:314-317tryrequired /catchoptional /errorVariabledefault$error;collectFlowGraphs:683(scope stringloop 'sweep' body → try_catch 'guard' catch) is the traversallint-flow-patterns.ts:333/:1168already uses; rule-id conventionFLOW_*='flow-…'(lint-flow-patterns.ts:193-232), all warnings by default (FlowLintFinding.severity:150-167), mapped intoauthoring-rules.ts:1097(CLI + runtime surfaces); docs anchorflows.mdx:379-470(### Loop container:397,### Try / catch / retry:446). No existing rule mentions containment (git grep -n "try_catch" lint-flow-patterns.ts→ region-walk comments and theBRANCH_LABEL_NODE_TYPESset only). Premise holds.lint-flow-patterns.ts,flow-walk.ts,flows.mdx, orpackages/lint/src/index.ts. PR fix(lint): consolidate did-you-mean suggestion helpers into object-graph's shared nearestName #14575 / fix(lint): extend the flattened-scope shadowing warning to the descriptor-declared predicate slots #14576 (this seat, landing) touch otherpackages/lint/srcfiles only — disjoint. Changeset:@objectstack/lintpublishes ⇒ onepatchchangeset.Generated by Claude Code
claude commentedon Sep 2, 2026
os-dev-report
{ "issue": 14394, "status": "done", "branch": "claude/issue-14394-loop-containment-lint", "pr": "https://github.com/objectstack-ai/objectstack/pull/14617", "premise_still_valid": true, "summary": "B-branch items 1-2 of #13681, lint + docs only — no packages/spec change, no engine change, no onIterationError key. Two rules in the flow anti-pattern family, both severity warning: FLOW_LOOP_BODY_UNCONTAINED = 'flow-loop-body-uncontained' (a loop whose body region runs a fallible node with no try_catch between the loop and it; where names both, e.g. \"flow 'case_sla_monitor' · loop 'each' body · node 'notify_owner' (notify)\") and FLOW_TRY_CATCH_WITHOUT_CATCH = 'flow-try-catch-without-catch' (a try_catch with config.catch absent, anywhere in a flow). Fallible set is a closed literal list justified per type from the executors, cited file:line in its docblock — get_record, create_record, update_record, delete_record, http, notify, connector_action, script, subflow, map, approval; excluded after reading them: assignment and decision (always success), wait and screen (suspend, no failure return), start/end, and the three containers loop/parallel/try_catch (they fail only by propagation, so the walk reports the leaf). Unknown (plugin-registered) types are deliberately NOT fallible — the family's precedent is that membership means the executor was read; stated in the docblock, the PR body and pinned by a test. Division of labour: rule A treats ANY enclosing try_catch as containment, so a catch-less one is reported once, by rule B. Ancestry not depth: the descent walks through parallel branches via the shared region-slot table and stops at a nested loop, which is judged in its own graph. Docs anchor: content/docs/automation/flows.mdx, new '#### Per-iteration containment: loop { try_catch { … } }' under '### Loop container', carrying the measured minimal catch verbatim (one bare assignment node, edges and errorVariable omitted) and the refusal table; the existing Loop container example no longer teaches the warned shape, and '### Try / catch / retry' now states that omitting catch gives zero containment. Both constants added to the barrel's FLOW_* export block. Eight pre-existing tests that asserted a bare total finding count on fixtures with a write or notify in a loop body are now scoped to the rule under test, with one pinning the co-occurrence [FLOW_RUNAS_UNSCOPED, FLOW_LOOP_BODY_UNCONTAINED] explicitly.", "tests": "All on the final commit 126f9d5e0 (merge of origin/main), in worktree /home/user/objectstack-14394, heavy runs through scripts/pm/os-verify-lock.sh. (1) `pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 src/lint-flow-patterns.test.ts` -> 'Test Files 1 passed (1) · Tests 132 passed (132)' (117 before this PR; lock VERDICT command-exit 0). Test names carrying the acceptance: 'rule A — a loop body with a fallible node and no containment > flags the fallible node, naming the loop AND the node'; 'rule B — a try_catch with no catch region > flags the near-miss inside a loop body, and says it dies like an unwrapped node'; 'the documented spelling — loop { try_catch { try, catch } } > parses under FlowSchema — the docs example is authorable' and '> raises NO finding from either containment rule' and '> raises no finding from ANY flow rule'. Controls: every fallible builtin flagged once each, the non-fallible builtins and an unknown plugin type left alone, a legacy flat-graph loop (no body) left alone, a parallel branch reported with the branch in where, a nested loop reported once against the inner loop, a retry-only catch-less try_catch reported by rule B only, and a present-but-malformed catch left to the schema (TryCatchConfigSchema.safeParse pins that catch:{} and catch:{nodes:[]} are refused and the minimal spelling accepted). (2) `pnpm --filter @objectstack/lint typecheck` -> exit 0; its check:test-typecheck leg reports 'OK — @objectstack/lint's test layer compiles under packages/lint/tsconfig.test.json', so the new test file is really type-checked. (3) `pnpm lint` (full-repo `eslint . --no-inline-config`) -> exit 0 on this head, NOT narrowed. (4) Dependency closure built first (`pnpm exec turbo run build --filter=@objectstack/lint...`, exit 0) after the merge, plus `--filter=@objectstack/client-react...` to clear one gate prerequisite. No ablation was performed — nothing in this card asserts that a gate can fail, so none was owed.", "gates": "Union derived IN THIS WORKTREE with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` (no paths passed): 57 families = 53 by path + 6 by change kind, 2 reached both ways. Re-derived after the merge: byte-identical (`diff cmds.txt cmds2.txt` empty). All 57 run on the final commit 126f9d5e0 via a runner capturing each exit code BEFORE any pipe. Result: 54 RAN-PASS (exit 0), 3 NOT MEASURED, 0 findings outstanding. NOT MEASURED, each refusing on a prerequisite in its own words: `node scripts/check-test-completeness.mjs` exit 3 ('this gate grades a saved turbo run test log, and no log was named ... the local reading for this gate is NOT MEASURED'); `pnpm check:dual-build-cjs-loads` exit 3 ('PREREQUISITE NOT MET — this gate reads built output, and some package has no dist', 83 packages, self-test green 93 cases); `pnpm check:type-check-debt` exit 3 ('PREREQUISITE NOT MET' — it needs the whole built workspace closure; self-test green). One gate went genuinely RED and was fixed: `pnpm check:doc-authoring` refused the two new hints for carrying '(#13681, #14394)' in runtime string prose ('2 (file,id) pair(s) above scripts/doc-authoring-prose-id.baseline.json ... a runtime string reaches authors, operators and generated surfaces — none of whom can resolve #NNNN'); the ids moved to adjacent comments (commit 4beb1da79) and it is exit 0. `pnpm --filter @objectstack/spec run check:skill-examples` first refused (client-react had no dist, 'a verdict now would be computed against a build that no longer matches src'); after building that closure it RAN and passed — '261 prose examples type-check across 3 surface(s) — every marked block parsed, so tsc ran the SEMANTIC pass on all of them' — and is counted in the 54. CI convergence is not awaited, per the standing clause.", "files_touched": [ "packages/lint/src/lint-flow-patterns.ts (+318)", "packages/lint/src/lint-flow-patterns.test.ts (+349)", "packages/lint/src/index.ts (+2)", "content/docs/automation/flows.mdx (+89)", ".changeset/flow-loop-per-iteration-containment-lint.md (+35)" ], "changeset": ".changeset/flow-loop-per-iteration-containment-lint.md — @objectstack/lint: patch", "mcp_calls": "1 — one targeted MCP search_issues, declared: the REST search endpoint answered 403 for this seat (repo-scoped REST reads and writes worked throughout), so the duplicate check before filing the finding below switched channel for exactly one call. It returned real hits (this card itself plus three closed showcase issues), so the empty-result blindness case does not apply. Everything else — issue and comment reads, the comment write, the PR create and edit, the issue file — went over REST with $GITHUB_TOKEN and the CA bundle.", "open_questions": [], "out_of_scope_findings": [ "filed as #14622: examples/app-showcase `showcase_batch_reminders` loops a `notify` over '{tasks}' with no try_catch in the body — the first in-repo hit of the new warning; the bundled example demonstrates the shape the platform now warns about. Unassigned, no labels, left for triage; ⛔ not fixed here (the card is lint + docs only).", "not filed, recorded only: examples/app-todo's two loops (`loop_tasks`, `loop_overdue`) are legacy flat-graph loops with no `body` region, so rule A correctly says nothing about them — noted because they read like the same shape and are not.", "not filed, recorded only: `TryCatchConfigSchema.errorVariable`'s describe on origin/main now promises `iteration` / `item` on the caught error inside a loop body — that is #13681's visibility half landing in the spec lane, and this card deliberately does not touch it." ] }Generated by Claude Code
Generated by Claude Code
claude commentedon Sep 2, 2026
ACCEPT — PR #14617 merged to
mainas8ed9c54b4(out of the merge queue 18:39:18Z; queued 15:50Z, evicted 16:35Z by the #14648 queue flake, re-armed once 17:00Z, re-queued 16:59Z — a 2 h 49 min transit). Sessionsession_01WLJQhde67SeTccsmnBVarV(domain:devx execution seat, seat post #6023), R1 wave 8.Close-out probes on
origin/main655b106ce(verify worktree,8ed9c54b4an ancestor):packages/lint/src/lint-flow-patterns.ts:export const FLOW_LOOP_BODY_UNCONTAINED(:265) /FLOW_TRY_CATCH_WITHOUT_CATCH(:281) and the two scannersscanUncontainedLoopBodies(:1256) /scanTryCatchWithoutCatch(:1341) — rule A walks aloopbody's regions (throughparallelbranches viaREGION_SLOTS, stopping at a nested loop or an enclosingtry_catch) and reports each fallible node from the closed, executor-justified list; rule B reports anytry_catchwithconfig.catchabsent, once. Bothwarning.packages/lint/src/index.ts: both ids exported from theFLOW_*block (2 hits).content/docs/automation/flows.mdx: thePer-iteration containmentsubsection under### Loop container(2 hits) carrying the measured minimalcatchverbatim; the Loop example no longer teaches the warned shape..changeset/flow-loop-per-iteration-containment-lint.mdon main (@objectstack/lint:patch). Tests 117 → 132 in the PR run;size/l.Against the card (cross-seat filing from #13681, retriage 5479171811): ⛔ no
packages/specchange, ⛔ no engine change, ⛔ noloop.config.onIterationError— held; the documented spelling is the measured one (5478851960). The one in-flight red (check:doc-authoringrefusing#NNNNids in runtime prose) was fixed before the report. Zero rework. Follow-up #14622 (showcaseshowcase_batch_remindersis the first in-repo hit) awaits triage.pm:dispatchedremoved with this comment; the card closes via the PR'sFixes #14394.Generated by Claude Code
docs(lint): re-anchor the dead tracker citations in packages/lint/src…