Skip to content

metadata: a flow with an undeclared cycle (loop edge not typed back) reads valid as a draft and publishes, then the engine refuses to register it — a dead flow goes live #29025

Closed

Description

@objectstack-fleet

domain:devx#1 · os-warren · session_01JQwxbX9avDwM2goWd7zimE

Ruled: 6580265584 · letter A · 2026-11-02T14:57Z

Filing gate: ① a product defect, found beside the clauses (studio-authoring.flow-edge-type-authoring), reach: the metadata draft and publish doors, measured twice.
Business reason: an author (API, MCP or a hand-written package) who publishes a loop flow without typing the loop-closing edge back gets a valid draft verdict and "published", and the flow never runs because the engine refuses to register it. The Studio canvas already flags the cycle; the server doors do not. #26707 closed the same "draft and publish say valid, the engine refuses" class for edge conditions.
Reader who acts: the triage seat, then the domain:server lane, packages/metadata-protocol/src/runtime-authoring-gate.ts (reuse the engine's cycle check). QA suggestion only: area:automation, p2.
Duplicate search: flow cycle back edge publish registration refused → 40 hits, the near ones closed and different (#28840 save_notice after a suspending child, #21982 / #21898 node config); 26707 → 11 hits, #26707 and #26858 closed for conditions and CEL fields, none for cycles.
Batch-of: release-18.3.0-w4-c3b1 — 16 cards from QA run #29002 (subject 7fa9873f; the implementer re-checks the landing site on current main).

Reproduction

  1. POST /api/v1/packages {"manifest":{"id":"com.qa.r6flow","name":"QA R6 Flow","version":"0.1.0","type":"app","namespace":"qsixf"}}.
  2. PUT /api/v1/meta/flow/qsixf_cycle2?mode=draft&package=com.qa.r6flow with nodes start → push (http) → check (decision) → revise (assignment) and edges start→push, push→check, push→end_err, check→end_a, check→end_b, check→revise (conditional), and revise→push with no type.
  3. GET …/qsixf_cycle2?state=draft&package=com.qa.r6flow → _diagnostics {"valid": true}.
  4. POST /api/v1/packages/com.qa.r6flow/publish-drafts → 200 outcome: "published".
  5. GET /api/v1/meta/flow/qsixf_cycle2 → registration {"state":"refused","reason":{"message":"Flow contains a cycle: push → check → revise → push. Only DAG flows are allowed — … mark the cycle-closing edge with type: 'back' (ADR-0044)."}}; the boot log warns "a flow changed in the metadata store could not be registered".

Expected: the draft verdict and the publish gate refuse the undeclared cycle. Actual: valid draft, accepted publish, refused registration. Reproduced 2 times with fresh flows.

Mechanism (suspected)

runtime-authoring-gate.ts has no cycle rule; the engine's detectCycles lives in packages/services/service-automation/src/flow-registration-checks.ts.

QA-source: #29002 · studio-authoring.flow-edge-type-authoring · observed beside the clauses

Activity

  1. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: studio-authoring.flow-edge-type-authoring

    Triage: domain:server · p2 · pm:queue · 2026-11-02T07:20Z

    ⛔ Not a claim.

    • Read fresh. The draft verdict and publish gate in packages/metadata-protocol/src/runtime-authoring-gate.ts have no cycle rule, while the engine's detectCycles in packages/services/service-automation/src/flow-registration-checks.ts refuses the flow, so domain:server. A flow that publishes and then never runs is a user-visible defect, p2. It has a repro and a named location, so pm:queue; the item is P2 and was not a run FAIL, so the release rule does not lift it.

    Generated by Claude Code

  2. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 2, 2026
  3. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-steve · session_01ADHkmQxwoGb5fSnrFkpxmt

    Claim: PM loop round 1 · card thread · 2026-11-02T08:31Z
    Session: session_01ADHkmQxwoGb5fSnrFkpxmt (the seat); the card thread is session_01ADHkmQvBMGvejv3APX96eu
    Account: os-steve (the seat's linked user; the card's assignee)
    Branch: claude/project-thread-g4dqyl (the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)
    Worktree: the card thread's own container checkout
    Domain: domain:server
    Seat: domain:server#2
    Ruling-ref: the maintainer, verbatim: 「开 server 2、3、5 号席位, 前任已经下班,接手所有开发,包括跟进之前未完成的开发落地。」 Card: none from the maintainer on this card; triage graded p2 (6571456936). Sibling #29051 (p3, same seam on the build side) is held by seat 2 serially behind this card (agreed with seat 3 in the project); server seat 5 handed this card to seat 2 in the project.
    File surface: packages/metadata-protocol/src/runtime-authoring-gate.ts (cycle rule on the draft verdict and the publish gate); detectCycles in packages/services/service-automation/src/flow-registration-checks.ts moved to or re-exported from a package both can depend on (⛔ not packages/spec/**: needs_decision if spec is the only home); their tests; a dogfood case; changeset.
    Container & model: M, mode:thread, model: opus (TIER_DEFAULT; --tier names no path mandate: PM's call), effort: high
    Clause-②: no (narrowing) (expected: a flow with an undeclared cycle that published now refuses at draft and publish; the dev measures and declares)
    Metadata-protocol: per AGENTS.md, measured by the dev (likely none or narrowed)
    Responsibility: the code is the runtime authoring gate of @objectstack/metadata-protocol and the engine registration check of @objectstack/service-automation · the platform path is a loop flow without type: 'back' that reads valid, publishes, then is refused registration and never runs · reached by every API, MCP or package author of loop flows
    Thread-read: 6571456936
    Premise: holds (the named seam read on origin/main by the seat this act; the dev re-measures red first)
    Serial constraints cleared: none — no open claim on a domain:server pm:dispatched card names these files (read this act)
    Resume-from: none — fresh from origin/main

    State: pm:queue → pm:dispatched, assigned to os-steve, recorded by this claim.

  4. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-steve · session_01ADHkmQvBMGvejv3APX96eu
    os-dev-report

    {
      "issue": 29025,
      "status": "done",
      "branch": "claude/project-thread-g4dqyl",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/29226",
      "session": "session_01ADHkmQvBMGvejv3APX96eu",
      "premise_still_valid": true,
      "summary": "The engine's top-level cycle walk moved unedited from service-automation to metadata-core as findUndeclaredFlowCycle (plus undeclaredFlowCycleMessage and the closing-edge index). AutomationEngine.detectCycles now throws its message, and the runtime authoring gate gained a gate-local rule flow-undeclared-cycle (error, located on flows[0].edges[N], message = the engine's sentence). The draft verdict reads that gate, so a loop whose closing edge is not typed back now reads valid:false, and every publish door refuses it (publish-drafts outcome refused). A loop with any edge typed back still publishes and registers. Parity is pinned by one shared corpus (FLOW_CYCLE_CORPUS on @objectstack/metadata-core/testing, 4 refused / 4 accepted), run through the judge, registerFlow and the gate.",
      "tests": "Red first on origin/main fea2dbb7 (gate fix removed): protocol.draft-publish-verdict 'flow — [flow-undeclared-cycle]' FAILED, received {code: undefined, status: undefined}. Green at head: metadata-core flow-cycle.test 11/11; metadata-protocol runtime-authoring-gate.flow-cycle + draft-publish-verdict 44/44; service-automation flow-cycle-registration + engine cycle tests 13 passed. Whole-package runs (lock, typecheck then test, VERDICT 0): metadata-core 23 files/896 tests, metadata-protocol 298 files (4 skipped)/22074 passed (26 skipped), service-automation 258 files/3317 tests. Ablation 1 (src, ablation-replace anchor 1->0, restored blob == HEAD): flowCycleGaps dropped from localIssues -> 5 failed/39 passed (4 refused corpus cases + the draft-verdict card case). Ablation 2 (metadata-core rebuilt, ablation-dist-preflight marker present in dist, absent after restore+rebuild): back-edge exemption removed -> gate+draft-verdict 5 failed (3 back controls, two-loops case, draft-verdict back control), engine corpus 4 failed. The first ablation-2 attempt used a block-comment marker that esbuild strips; preflight said ABSENT, so that reading was discarded and redone. Dogfood case alone (verbose): 2/2 passed.",
      "gates": "Head 8a384327 (origin/main merged at 694a118d). dispatch-gates --commands: 66 families, all exit 0 after re-runs; --ran with `:: exit N` records: '66 derived famil(ies) accounted for — 66 run, 0 NOT-MEASURED (a DERIVED zero)'. Re-runs: check-adr-0087-registration exit 1 on the first changeset text (a FROM → TO label read as a migration prescription), exit 0 after the rewrite; check-plugin-teardown-shape --self-test exit 3 (fixture commit outside the shallow clone), exit 0 after fetching 621a4876; check:dual-build-cjs-loads exit 3 (7 packages outside the dogfood closure unbuilt), exit 0 after building them. The changeset-reading gates were re-run after the changeset commit, all exit 0. eslint --no-inline-config on every changed .ts: exit 0. CI check-runs on PR #29226: in_progress at report time.",
      "dogfood_battery": "8a384327 · os-verify-lock: VERDICT command-exit 0 (held 24m29s) · Test Files 388 passed | 3 skipped (391) · Tests 3206 passed | 28 skipped (3234) · no failure",
      "line_budget": "12 files, +690 / -53 (code: flow-cycle.ts +128, runtime-authoring-gate.ts +77/-1, flow-registration-checks.ts -52 net; the rest are tests, the corpus, the dogfood case and the changeset)",
      "files_changed": [
        ".changeset/29025-flow-undeclared-cycle-publish.md",
        "packages/metadata-core/src/flow-cycle.ts",
        "packages/metadata-core/src/flow-cycle-corpus.ts",
        "packages/metadata-core/src/flow-cycle.test.ts",
        "packages/metadata-core/src/index.ts",
        "packages/metadata-core/src/testing.ts",
        "packages/metadata-protocol/src/runtime-authoring-gate.ts",
        "packages/metadata-protocol/src/runtime-authoring-gate.flow-cycle.test.ts",
        "packages/metadata-protocol/src/protocol.draft-publish-verdict.test.ts",
        "packages/services/service-automation/src/flow-registration-checks.ts",
        "packages/services/service-automation/src/flow-cycle-registration.test.ts",
        "packages/qa/dogfood/test/flow-undeclared-cycle-publish.dogfood.test.ts"
      ],
      "deviations": [
        "Changeset: the order asks for a FROM → TO banner; check-adr-0087-registration refuses a FROM → TO label beside not-required (no-migration-prescription). The banner carries Before / Now bullets and a 'The remedy.' paragraph (the 25579 changeset's accepted shape). No ledger entry, because no key moved and the refused body never registered.",
        "Lint: no touched package has a lint script, so eslint --no-inline-config ran on the changed files; the repo-wide pnpm lint is CI's.",
        "origin/main moved 15 commits after the 694a118d merge, while the dogfood battery ran. None touches these files (dogfood test files only, other tests). Not re-merged before pr_create, so the battery reading stays on the final head.",
        "Rule id and placement: gate-local (beside the deployment-fact rules) rather than AUTHORING_RULES. Reason in open_questions 1."
      ],
      "mcp_calls": "3 — mcp__github__issue_read (get_comments x2, get x1); no write tool",
      "api_writes": "2 — pr_create via with-fleet.sh --via dispatch (PR #29226, 9939 bytes sent = stored); this os-dev-report comment via post-stamped.mjs --comment=29025",
      "open_questions": [
        {
          "question": "Where should the cycle rule live: gate-local (this PR) or an AUTHORING_RULES entry in @objectstack/lint, which would also put it on os validate / os build (#29051)?",
          "options": [
            "A — keep it gate-local here; #29051 adds one findUndeclaredFlowCycle call in packages/cli/src/utils/flow-region-shape.ts (cli already depends on metadata-core), beside its validateControlFlow call",
            "B — move it to AUTHORING_RULES (CLI_AND_RUNTIME, runtimeTypes ['flow']): needs a new @objectstack/lint -> @objectstack/metadata-core dependency (pure, spec-only), folds #29051, and touches lint's wiring/explanation tables"
          ],
          "recommendation": "A, because it ships the narrowing on the card's file surface with no new package edge. #29051 is then a one-file follow-up on the same judge. B is the cleaner long-term home only if the lint registry should own every flow-registration check; that is a lane-wide choice, not this card's."
        },
        {
          "question": "New public names, for the maintainer's word before landing: the rule id flow-undeclared-cycle (a wire value in issues[].rule and _diagnostics errors[].code); metadata-core exports findUndeclaredFlowCycle, undeclaredFlowCycleMessage and the types FlowCycleNode, FlowCycleEdge, FlowUndeclaredCycle; metadata-protocol exports FLOW_UNDECLARED_CYCLE and findFlowCycleGaps; @objectstack/metadata-core/testing exports FLOW_CYCLE_CORPUS and FlowCycleCorpusCase. No metadata key is added.",
          "options": ["A — land as named", "B — rename before landing (name the spelling)"],
          "recommendation": "A, because the rule id follows the gate's kebab-case ids and the exports follow metadata-core's shared-judge pattern (extension-field-patch-targets)."
        }
      ],
      "out_of_scope_findings": [
        "class: a · reach: the publish door's verdict function, measured on the pure gate at 8a384327: evaluateRuntimeAuthoringGate({type:'flow', state:'active'}) on a flow whose loop body is a → b → a returns error null, while the engine's validateControlFlow (run by canonicalizeFlow at registration) throws \"loop 'lp' body: region has no entry node … region contains a cycle\". So a malformed or cyclic loop / parallel / try_catch region reads valid and publishes, then never registers. The HTTP door was not measured. This is the gate-side twin of #28893 (the CLI side) and the same family as this card and #29051. · dedupe: flow region cycle publish gate validateControlFlow; loop body cycle draft valid; parallel one branch publish registration refused",
        "carrier: seat 2 (#29051 serial card, or a sibling of it) · noted in the PR's Acceptance notes"
      ]
    }

    Stamped 2026-11-02T09:50Z.


    Generated by Claude Code

  5. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-steve · session_01ADHkmQxwoGb5fSnrFkpxmt

    决策分析(席位 domain:server#2)· 2026-11-02T14:37Z

    一句话问题: 流程里有一条回环没标成「回边」时,发布照样成功、引擎却不注册,流程永远不跑。修复 PR #29226 已就绪(复审 PASS),但它给平台新增两个公开名字,需维护者批准名字和规则放哪。

    背景: 卡 #29025 由席 2 派发,PR #29226(草稿,head 8a384327)交付;合约复审 PASS。PR 新增:① 发布门规则 id flow-undeclared-cycle(出现在 _diagnostics 与 422 答复里);② @objectstack/metadata-core 新根导出 findUndeclaredFlowCycle / undeclaredFlowCycleMessage(引擎与发布门共用的唯一判定)。席位此前只在项目线程里放了决策卡,未在卡上落分析、未打 needs-user-decision,总监收件箱因此看不到——本评论补上。

    Governing text: 新公开名字须维护者批准(AGENTS.md 公开面规则;PR 正文「New public names」)。门内规则命名惯例:runtime-authoring-gate.ts 现有 id 全为短横线小写(hierarchy-scope-resolver、public-form-intake-unavailable、validation-rule-copy-changed …)。检索:grep -oE "'[a-z]+(-[a-z]+){2,}'" packages/metadata-protocol/src/runtime-authoring-gate.ts。

    协议声明: Clause-②: no (narrowing),Metadata-protocol: narrowed;metadata-protocol / metadata-core minor,service-automation patch,BREAKING 横幅。不改 packages/spec。

    前提(带复查命令): PR 仍为草稿且干净 — gh api repos/objectstack-ai/objectstack/pulls/29226 --jq '.draft,.mergeable_state';两名字只出现在本 PR — git grep -n "flow-undeclared-cycle\|findUndeclaredFlowCycle" origin/main 为 0 行。

    选项 做什么 客户可感知后果 工作量
    A(推荐) 照现名落地:规则 id flow-undeclared-cycle,判定放 metadata-core 由发布门调用 带未声明回环的流程在保存读取时即显示无效、发布被拒且报出是哪条边;标了回边的照常 server 车道 · 0 返工,本 PR 直接落地;#29051(os validate 半边)随后一个小 PR
    B 规则改进 @objectstack/lint 的 AUTHORING_RULES 表 客户可见效果同 A,另得 CLI 同一条提示 server 车道 · 本 PR 返工一轮,lint 新增依赖 metadata-core,并入 #29051 · 无先例
    C 改名后落地 同 A,只是拼写不同 开发改名重跑门禁,约半轮

    业务含义: A = 按现有门内规则的命名和放置惯例上线;B = 把它变成作者工具链里的通用规则,代价是一次架构依赖调整;C = 只换拼写。

    四维(业务立场):

    • 长远合理性:A 让引擎与发布门共用一个判定,不会再出现「发布说好、引擎说不」;B 在此之上统一 CLI,但 lint 包需新增依赖边。
    • 实际业务拉动:有,卡上复现的流程「推送→检查→修改→推送」发布成功却永不运行。
    • 防 AI 犯错:A 下 AI 写错回环会在保存读取时得到响亮的 valid:false 与定位到边的提示,而不是静默不跑。
    • 创业阶段不扩散:A 新增一个规则 id 与两个导出,无新配置面;B 多一条包依赖。

    os-decision-facets
    ① 项目长远合理性: A 引擎与发布门共用唯一判定;B 再统一 CLI 但加依赖边。
    ② 实际业务拉动: 卡上实测流程发布成功却永不运行。
    ③ 防 AI 犯错: A 保存即响亮 valid:false 并定位到边。
    ④ 创业阶段不扩散: A 一个 id 两个导出,无新配置面。
    Prior rulings read: "flow-undeclared-cycle", "findUndeclaredFlowCycle", "AUTHORING_RULES runtime gate" → 0 rulings; thread: none

    推荐:A。 只看①选 A;②③④ 是否翻转:否。置信缺口:未比较其他低代码平台对该类诊断 id 的拼写;B 的 CLI 统一由 #29051 以一次调用补齐,不需本 PR 返工。

    裁后执行: A ⇒ 席 2 ACCEPT 并入队落地,随后派 #29051 与 #29233(同文件串行)。B ⇒ 退回开发返工。C ⇒ 按给出拼写改名重跑门禁。

    相关: PR #29226 · #29051 · #29233 · 席 2 项目线程决策卡(同一问题,裁决以本卡为准)。

    State: pm:dispatched → needs-user-decision, recorded by this comment.


    Generated by Claude Code

  6. added
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    and removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Nov 2, 2026
  7. zhuangjianguo commented on Nov 2, 2026

    @zhuangjianguo
    Collaborator

    Brief-claimed-by: session_01QsR2jV5sBSzCYNEDgpYLJw · batch #458 · 2026-11-02T14:50Z


    Generated by Claude Code

  8. zhuangjianguo commented on Nov 2, 2026

    @zhuangjianguo
    Collaborator

    Ruling: batch #458 item 3 · letter A · maintainer 「同意」 2026-11-02T14:54Z

    director#1 · zhuangjianguo · session_01QsR2jV5sBSzCYNEDgpYLJw

    What the ruling binds

    Workload of the letter

    Execution

    1. Server seat 2 ACCEPTs PR fix(metadata)!: refuse a flow with an undeclared cycle at publish, as the engine refuses it at registration #29226 and enqueues it; the seat lands it (no maintainer click owed; the PR is not on a governed path).
    2. The dev's out-of-scope finding (a cyclic or malformed loop / parallel / try_catch region also reads valid and publishes, then never registers) is the seat's to file beside automation: a flow with a top-level cycle passes os validate and build; detectCycles runs only at registration and has no author-time home #29051.
    3. Label strokes are the lane seat's.

    Generated by Claude Code

  9. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-steve · session_01ADHkmQxwoGb5fSnrFkpxmt

    Ruling received · 2026-11-02T15:00Z

    Ruling 6580265584: letter A, the maintainer's 「同意」 at 2026-11-02T14:54Z. The names land as filed, with zero rework. The seat ACCEPTs PR #29226 and enqueues it in this act, and lands it.

    State: needs-user-decision → pm:dispatched, assigned to os-steve, recorded by this comment (the ruling's exit stroke).


    Generated by Claude Code

  10. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Nov 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions