jeeflow工作流引擎
| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
ci(verify): pip 安装同样加一轮重试(PyPI JSON 200 后索引可能还差一拍) | 10 天前 | |
feat(spi,engine,facade,repository,extensions,model): issues/141 G10 空抄送人不建 cc 行 + G9 记录类(custom)节点按节点类型分流 两件都在 owner 2026-09-29 夜拍板,落同一支建单/cc 路径,故一并提交、分两段说清。 ────── 一、G10「空不创建行」(spec 06 §2.10,基准=java 5fbd5ac)────── 普查实测:本栈 parse_cc_actors 早就逐项 trim+丢空+按序去重,逗号串腿本来就合规; 洞在另外三处——① 门面手动腿走 _to_str_list 不过归一([''] 实测 code=0 且落 actor_id='' 的行) ② 两仓 create_cc_instance 与 SPI default create_cc_instance_if_absent 都只挡 is None、 都不 trim(直连仓储 ['',' ','8501'] 实测落 3 行)③ ' 123 ' 与 '123' 判成两个人, 把上一轮 G2 的写侧判重打穿。 判据单点新增 spi.normalize_cc_actors(逐元素 str+trim、空串/纯空白丢弃、同次折叠、顺序保持); 放 spi 层与 java 放 StringUtils 同理——漏斗(engine.parse_cc_actors、门面手动腿)与写侧 (两仓 create_cc_instance、SPI default)四层都得吃到,而仓储实现不该反向依赖引擎模块。 parse_cc_actors 只留形态层(逗号串拆元素)后委托它 ⇒ 两形态同判据。 '0' 这类"看起来像空"的正常 id 明确不丢(判据只按 strip() 后是否为空串,不许写 if not actor)。 手动腿丢完为空 ⇒ 沿用既有 actorIds 缺失档,不新造错误码/文案。 ────── 二、G9 记录类节点没有参与者是正常形态(spec 02 §6.1;撤回 ac8b557 的兜底那一半)────── owner 原话「这个得根据任务类型来,自定义类型这种记录类的,不会有参与人,是正常行为」。 旧形状是 if node.type in (TYPE_TASK, TYPE_CUSTOM) 一起进 _create_task,参与者为空时 兜底挂当前操作人建一行 DOING ⇒ 伪造一条他不该收到的待办(spec §6.1 明写禁止的形状②)。 现按类型分流:任务类仍走 _create_task(解析为空仍建行,java CreateTaskHandler 无条件建单, 这一侧不动);记录类新增 _exec_custom_node —— 解析 clazz 处理器 → 落一条 task_state=20 历史行(新增 model.ProcessInstance.create_history_task,形状照 java ProcessInstance:425: 参与者=当前操作人只作留痕主体、无 form、无 expireTime、会签字段 0、parentTaskId 与 行级 isFirstTaskNode 建单不变量照旧)→ 令牌沿出边继续流转。不发码 3(码 3 表达"新待办产生")。 新增 extensions.ICustomHandler + HandlerRegistry.register_custom/resolve_custom (python 无从反射 JVM 类名,与 C# 栈同策按名注册;返回值非 None 写进 val,缺省键 custom_return_val 对齐 java FlowConst.CUSTOM_RETURN_VAL)。 ⚠️ 一处刻意分歧挂账待拍:clazz 未注册时本栈记 WARNING 后照常落历史行+推进, 而 java/C# 是显式报错「自定义模型[class=...]实例化对象失败」——理由是沿用共享夹具 flows/08-custom-node.json 里的 JVM 类名在 python 必然解析不了,报错会让该定义在本栈跑不了, 属栈限制不是语义缺陷。要统一到"显式报错"须 owner 拍,见 hub issues/141 §14。 tests/demo_reset_test.py 的状态读数按新形状**重算** 16/9 ⇒ **15/10**(实例总数 25 不变, I15 用的就是 define=10 那条 custom 流程,改回 java 同构形状后发起即办结); 文件头与断言行都写明"这是跟着裁定重算数值、不是改期望蒙红",旧数值出自被撤掉的兜底做法。 demo/main.py、demo/seed_business.py 同步注记"种子行数≠落库状态读数"。 测试:新增 16 格(G10 十格含两仓兜底/SPI default/两仓同答案/手动腿空档;G9 五格含 记录类不落待办、历史行不变量+不发码 3、clazz 返回值落 vars、未注册 clazz 仍落行并推进) + T1 真库 11 格。既有期望除 demo_reset 那一档数值外零改动。 逐处还原实测改前会红:摘掉归一单点 ⇒ 12 failed(含 1 格既有 cc 腿测试一起红); 把记录类并回任务类 ⇒ 5 failed(demo_reset 15/10 + 四格 G9)。还原后 md5 与修好版一致。 读数:pytest T0 224 passed(原 208),T1 连 160 真库 197 passed/0 failed(原 186)。 | 3 天前 | |
feat(spi,engine,facade,repository): issues/142 B 批 归属值写侧归一搬到任务侧(spec 06 §2.11,复用 §2.10 那一枚单点) owner 2026-09-30 拍「八栈一起收:两形同判据+写侧兜底+trim+哨兵」。派工会话产出, 由我复核后独立复跑门禁再提交。 终局读数(我本机复跑,非转述): - pytest **246 passed / 0 failed**(基线 231,未改任何既有期望值); - 真库 PYTHONIOENCODING=utf-8 .venv/Scripts/python.exe tests/jdbc_test.py mysql **223 passed / 0 failed**(基线 197 ⇒ 只增不减;postgres 腿按 issues/118/160 裁定豁免)。 单点:normalize_cc_actors → **normalize_actors**(逗号拆串收进单点 ⇒ 两形结构上不可能分叉), 旧名保留成**同一对象别名**;另加标量档 normalize_actor_value 与主键档 require_present_id。 五处落点(逐字对 §2.11 表):addCandidate/surrogate、transfer 三值、f_/tf_nextNodeOperator 两条消费腿 (旧形状 [str(a) for a in next_op] 把 None 串化成 "None" 落进归属列)、 updateCCStatus 的 operator 归一后再比、两仓 add_task_actor 写侧兜底(trim+丢空+判重+空主键 ValueError)。派工自报的改前红样我核过读数原文,摘三条最值钱的: 两仓不同答案 内存 {9:['c',' c ']} vs SQL {9:['c','c',' c ']}(同一入参两套尺子) 真库 ⑰ 段崩溃 IntegrityError (1048, "Column 'process_task_id' cannot be null") ['leader', ' 8123 ', '', ' ', 'None', '8123'] —— 数组形八元素全落,含串化的 None 另两档语义变化如实记进 changelog:标量 actorIds(如 123)从"报缺参数"变有效参与者; 两仓 add_task_actor(空主键) 从静默落库改抛 ValueError。第三方裸调 create_cc_instance(iid, c, "a,b") 现在拆成两行(与漏斗同判),是有意的口径收紧。 一处事故如实留痕:派工会话在崩溃那轮跑在 ⑮ 清理之前,给 160 库留了 54 条委托行, 已按显式 id 精确删除并复跑端到端全绿(前缀式 purge 删不掉无前缀行=issues/123 那条老坑的再现, 这次是崩溃路径逼出来的)。 三条待裁我这边定了(同裁定约束其余栈,记 issues/142 §9.1): ① ' ' 纯空白**丢弃**是对的——条文(§2.10/§2.11)明写"空串、纯空白一律丢弃", 我派工单里把 ' ' 列进"四个人"是**我写错了**;四个人指的是 '0'/'00'/'1'/'01'。 ② 错误文案跨栈不统一属条文允许(要求③"沿用各栈既有信封、不新造"),门禁只判 code≠0。 ③ 没动的同类病灶收下但记账,别当已修:引擎 assignee 变量腿 str(x) 串化、 save_task/update_task/_insert_task_actors 通道不过归一、remove_task_actor 不 trim、 _operator_arg 返回未 trim 原值(129 地界);历史脏行不追溯(同 G2"接受既成事实"口径)。 | 2 天前 | |
fix(engine): issues/126 案 A 任务行 expire_time 五处写点按节点表达式真算(基准=boot2) 本栈此前无到期表达式求值器(§1.5 普查),建单/会签/回退一律不赋这一列 ⇒ 常规流上逾期统计恒 0。 - jeeflow/engine.py:803 process_time(expr, args) 三档逐字对齐 Java FlowUtil.java:64 (变量档优先→datetime/毫秒/契约文本,类型不认识**落穿**;Ns/Nm/Nh/Nd,d 走名义日历加; 绝对档严格 %Y-%m-%d %H:%M:%S;失败 None,任何分支不出现 now());:858 _apply_expire_time 未配(None/空串/纯空白)⇒ 早退,该列留 None - 五处写点::532 普通建单 / :515 串行会签首位 / :504 并行全员 / :524 其它会签档(同并行)/ :325 回退新建(变量源=随行 nt.variables=boot2 hisVariable) + :191 **第五处=串行推进出的下一位成员**(依据:boot2 推进回调 createCountersignTask ProcessTaskServiceImpl:485,内含 :524 到期写;java 侧同形态见 cb541d4 的 applyNodeExpireTime) - tests/spec_test.py 新增 12 格(T0 四格 + 求值器档位三格 + 并行全员 + 串行首位与推进位 + 回退新建两格〔含"变量源必须用随行不是实例"〕+ 未配两档负向);负向格把"行没读到"与 "值为空"分开断,①的判据是**同一行** expire−create ∈ [2h−5s, 2h+60s] 而非"非空" - 共享夹具 06-countersign-sequential-expire.json 随 java 镜像进本栈 flows(16 份) 三场景:正向 / 负向 / 回归 PYTHONIOENCODING=utf-8 .venv/Scripts/python.exe -m pytest tests/spec_test.py -q ⇒ **125 passed**;连带 metadata/test_meta/test_bare_import/test_persist 合计 157 passed; e2e_test.py 63 PASS/3 FAIL 与改前基线逐格同名同数(三条都在 startAndExecute 预指派人,既有失败)。 既有断言零改动:git diff --numstat tests/spec_test.py = 273 增 **0 删**;engine.py 113 增 2 删 (那两删是本批自己写的注释与 import 行,HEAD 里不存在被改的期望值——已核 git show HEAD 零命中)。 变异对照两层:①摘掉 :191 第五处 ⇒ 推进那格红在"配了到期表达式的行必须带 expire_time"; ②未配时若回落 now() ⇒ 负向格红(证它不是恒真)。两次均 cp .mutbak 回写并核 sha256 008b3c8317979ed0… == 改动后基线,.mutbak 已删。 未跑:T1 tests/jdbc_test.py(连 160)、L2-27 跨栈门禁、镜像轮。 只报不改(按卡 §1.9):_create_task_with_actors(engine.py:383) 全仓零调用者;实例级 inst.expireTime 本栈无任何赋值点。 | 4 天前 | |
fix(facade): issues/137 §3-1 门面出口固定文案「流程处理失败」+判别式五条(python) owner 2026-10-02 第 3/4 问拍 A;spec 先行已落(doc 56f359e 新开 §2.12),本栈照它落地。 改前主泄漏点:jeeflow/facade.py:101 顶层 flow() 的 return self._error(str(e))——本栈唯一出口, 任何异常的 str(e) 都原样外透。解析腿批二已按 issues/139 做过 cause 分离 (raise ValueError(MSG_READ_DEFINE_JSON_FAIL) from e),本轮**未改其代码**,只补 docstring 说明它是 §2.12 的覆盖面第 ② 处,并把两处失真注释(原句「门面顶层用 str(e) 出 msg」)更正。 **普查结论(比 §1.5 更严,且决定了判据形状)**:对 HEAD 快照逐文件 AST/grep 实数—— 自定义异常类定义 **0 个**;引擎 raise 的类型只有两种内置类型:**ValueError 102 处** (facade 78/engine 13/persist 7/spi 2/meta 1/model 1)+ NotImplementedError 1 处 (meta.py:100,无参);引擎里 raise 运行时族类型(TypeError/AttributeError/KeyError/ IndexError/ArithmeticError/RuntimeError/OSError/JSONDecodeError…)**全部 0 次**。 ⇒ **java 那边是"约十处裸 RuntimeException 携带契约文案",本栈是 102 处裸 ValueError**, 一刀切把 ValueError 判内部的破坏面比 java 大一个量级 ⇒ 第 4 条**必须给 ValueError 开例外**。 - INTERNAL_FAILURE_MSG = "流程处理失败"(与 java **同名同值**,八栈可 grep 互查;逐字未改措辞)。 - is_foreign_detail(exc_type, message, cause, trace) —— **四入参签名逐位对齐 java 的 isForeignDetail(type,message,cause,trace)**;命名取其 snake_case 直译,与本栈既有 normalize_actors/require_present_id 同风格且跨栈 grep 一眼对得上。 配套 runtime_internal_detail(≈ java jvmInternal,**多收一个 message 入参**:python 没有 NumberFormatException 这个独立类型,数字解析腿只能按 CPython 内置文案模板判)、 thrown_inside_engine(**与 java 同名**)、以及 python 专有的**抽取层** foreign_detail_of(exc) (零判据逻辑,只从活异常拆 (type, str(exc), __cause__ or __context__, reversed(extract_tb)); java 不需要它,因为 getClass()/getMessage()/getCause()/getStackTrace() 本就是分开的调用)。 - 第 4 条三腿互补,逐条都有测试钉:a 腿类型族**不含 ValueError**,只收它的两个非契约子类 UnicodeError/json.JSONDecodeError(引擎零处使用,普查已证);b 腿用 \A 锚定 CPython 数字/时间解析文案模板(could not convert string to float/invalid literal for int() with base N/ Invalid isoformat string/time data … does not match format/unconverted data remains 等), **模板不写死**——由 test_criterion_4_numeric_parse_templates 当场把 float('x')/int('x')/strptime 调炸取真实文案来钉,CPython 换版本漂了当场红而不是静默绿;c 腿 __module__ != "builtins" ⇒ 内部 (**这一腿是本栈能在"核心零依赖"前提下认全 DB 驱动异常的唯一办法**:aiomysql/asyncpg 都是 optional-extra、TYPE_CHECKING 惰性引用,驱动异常全都只 extends Exception、类型名零共性、 不能 import 来 isinstance)。 - 第 5 条用 traceback.extract_tb 的帧归属(抛出点在 jeeflow/ 内 ⇒ 透出)。 ⚠️ 关键发现:repository/base.py:224 的 except BaseException: rollback; raise **裸重抛会把 base.py 压成栈顶帧** ⇒ 第 5 条会误判成"引擎写的",只有第 4c 腿能兜住驱动原文—— **这一腿就是 c 腿存在的理由**,专门加了 test_criterion_4_driver_and_third_party_types_are_internal 把栈顶帧**故意钉在 jeeflow/repository/base.py** 来验它。 - 顶层出口:命中 ⇒ _log.error("[jeeflow] action 执行失败: action=%s", action, exc_info=e) + 返回固定文案;未命中 ⇒ self._error(str(e))。原文只进 logging(exc_info=e ⇒ 完整异常对象 +栈+它自己的 __cause__)与错误对象的 __cause__,msg/data 一律不含原文。 - 另加一枚**前提回归钉** test_engine_module_never_defines_its_own_exception_type:用 ast 扫全仓 ① 零自定义异常类 ② raise 的类型名集合 ⊆ {ValueError, NotImplementedError} ③ ValueError 载体数 ≥ 100。哪天有人给引擎加了契约异常类型(第 2 条就需要落地)或换了载体类型 (第 4 条例外的前提就废了、RuntimeError 契约腿会被静默mask),这一格会红并把处置写在断言消息里。 用 AST 而非 grep ⇒ 本轮新增的注释文本不会污染计数。 **按 owner 2026-10-02 裁定改了一格既有测试的读法(本轮唯一一处,红线要求先停下回报、已回报并获批)**: tests/spec_test.py:7022 test_i142_62_custom_handler_own_exception_still_propagates 改前断言 r2["code"] != 0 and "handler-boom" in str(r2["msg"])——后半钉的正是 §2.12 要挡的形状 (**集成方自己写的 custom handler 抛的原文**),它 :7019 的注释还写着"顶层 catch 把消息原样送出", 等于把泄漏机制当成了判据的一部分。改成两条独立断言:code != 0(**本格的真实意图**: 处理器自身炸不得被吞成成功、不许顺手降级成"记日志继续"把数据写脏)+ msg == "流程处理失败"。同格 :7016 的**引擎直用腿**(pytest.raises(RuntimeError, match="handler-boom")) 打的是引擎 API 不是门面出口、不受 §2.12 约束,**原样保留** ⇒ 这是**改读法不是改判据**。 铁证:变异 M1(出口还原 str(e))下这一格是**唯一变绿**的既有测试 ⇒ 它本就是一枚钉住泄漏的测试。 java 基准同形状也出固定文案(isForeignDetail 第 5 条:抛出点 com.example.Boom 不在引擎主包), 而 java 无测试钉它 ⇒ 这本就是一处未立案的跨栈分叉,本轮由 python 侧收口。 新增 tests/test_facade_internal_error_no_leak.py **25 格 / 809 行**:A 组 12 格纯函数 (五条判据各自可测 + **102 处契约文案反闸** test_criterion_4_never_misfires_on_engine_contract_value_error 用 57 条真实字面量 × 8 个模块帧逐条断言判「透出」 + AST 前提回归钉);B 组 13 格走**门面真实调用路径** (9 负向泄漏:AttributeError/TypeError/KeyError/数字解析 ValueError/JSONDecodeError/裸包装/ sqlite3 驱动错经 bizData 腿/第三方 provider/空 message;4 正向与解析腿:三种坏内容 ⇒ msg 逐字 读取流程定义 JSON 失败(**既不是解析器原文也不是 流程处理失败**)且门面零 ERROR 日志、 三条解析腿同句、__cause__ 是 JSONDecodeError)。 ⇒ 日志与 __cause__ 两半都有断言,防"把原文整个丢掉"也能绿的假修。 改前红样六组(tmp/137-3-1-python/mutation-137-3-1-python.py,字节级 shutil.copy2 备份 → CRLF 感知的字节级替换(锚点唯一性预检,命中数 ≠ 1 即中止)→ 跑 pytest → copyback → sha256 核对; 交付件 sha256 741e320f…,六组之后均还原逐字一致): · M1 出口还原 self._error(str(e))、判别式整个拿掉 ⇒ **9 failed**,全是我新写的泄漏侧格; **纯函数 A 组全绿**(M1 拆的是"接线"不是"判据")⇒ 这正是 spec §2.12 要求"走真实调用路径、 不只测纯函数"的实证:只写 A 组的话 M1 会全绿; · M2 判据收窄成"一律固定文案"(foreign_detail_of 恒 True)⇒ **36 failed**,含 **31 格既有测试** (test_i134_*×4、test_i139_*×2、test_i142_*×5、雪花 id 精度守卫、test_withdraw_*×2、 test_transfer_negative_matrix、test_guard_order_is_fixed_across_stacks 等) ⇒ "判据过宽=静默改契约面"在本栈不是理论风险,102 处 ValueError 契约文案背后有一整片既有测试看着 (java 侧这一片一处都没钉,本栈现在钉住了); · M3 一刀切把 ValueError 判内部(="只透契约异常族"在本栈的退化形)⇒ **40 failed**; · M4 删第 3 条 ⇒ 4 failed;M5 删第 4c 腿 ⇒ 2 failed;M6 删第 5 条 ⇒ 3 failed (三组各自只红对应那几条判据的格 ⇒ 五条互不顶替、各有独立覆盖面)。 读数(均我复跑非转述):PYTHONIOENCODING=utf-8 .venv/Scripts/python.exe -m pytest -q ⇒ **325 passed / 0 failed**(改前基线 **300**,300+25=325;含改读法那格转绿); 新格 **25 passed**;-k i142_62 **2 passed**。jeeflow/facade.py 行尾保持全 CRLF (改后 2020 CRLF / 0 bare LF),numstat 190 3 与 12 3,非整文件重写。 jeeflow/spi.py(§3-6 06a8d07)零改动;build/lib/、.venv/、__pycache__/ 未碰。 ⚠️ 四条如实记档(都不是缺陷,但下轮别当新问题重开): ① RuntimeError 在类型族里,而 NotImplementedError 是它的子类 ⇒ 引擎那唯一一处 raise NotImplementedError(meta.py:100,**无参**)会被第 1 条(message 为空)先挡掉; 改后出固定文案,比改前出**空 msg**(违反 §2.1「msg 为失败原因」)更好。 AssertionError 同理留在族里:引擎 assert x, "文案" 实数 **0 处**,不会误伤。 两处的安全性都由上面那枚 AST 回归钉看着(谁给引擎加 RuntimeError 契约腿它当场红)。 ② _FOREIGN_VALUE_ERROR_RE 的模板与 CPython 版本文案绑定(本机 3.11.15,requires-python >=3.10)。 失败模式是**响的**(测试当场调炸内置函数取真实文案再断言),但真漂了要同步更新正则。 ③ **新增了一处下游可见输出**:内部异常现在会在 logger jeeflow.facade 上出一条 ERROR(带完整 traceback)。 集成方若未配 root handler,Python 的 lastResort 会把它打到 stderr。与 java 的 log.log(Level.SEVERE, …, e) 同形、也与本仓 engine.py 既有 logging.exception(...) 惯例一致, 但**建议进发版说明**。 ④ 第 2 条在 python 是**文档化的空档**(本栈无契约异常类型):保留了注释位与八栈逐条可比对的编号, 并加了 AST 回归钉——一旦有人引入契约异常类型,那格会红并提示要落地第 2 条。 ⑤ 走的是 ① 出口层判别式(与 java 基准一致),没去改 moon 那种 ② 构造层;解析腿的 ② (issues/139 的 raise ... from e)原样保留——两者互补:② 让契约文案能逐字透出, ① 兜住任何形状的外来原文(含第三方 provider 与 base.py:224 的裸重抛)。 ⑥ processInstance/bizData 的 meta_reader 调用是**同步**的(return self._meta_reader.read_by_…(...), 无 await)——既有形状,与 §3-1 无关,按原样写测试桩;若那是缺陷需另立案。 | 23 小时前 | |
fix(facade): issues/137 §3-1 门面出口固定文案「流程处理失败」+判别式五条(python) owner 2026-10-02 第 3/4 问拍 A;spec 先行已落(doc 56f359e 新开 §2.12),本栈照它落地。 改前主泄漏点:jeeflow/facade.py:101 顶层 flow() 的 return self._error(str(e))——本栈唯一出口, 任何异常的 str(e) 都原样外透。解析腿批二已按 issues/139 做过 cause 分离 (raise ValueError(MSG_READ_DEFINE_JSON_FAIL) from e),本轮**未改其代码**,只补 docstring 说明它是 §2.12 的覆盖面第 ② 处,并把两处失真注释(原句「门面顶层用 str(e) 出 msg」)更正。 **普查结论(比 §1.5 更严,且决定了判据形状)**:对 HEAD 快照逐文件 AST/grep 实数—— 自定义异常类定义 **0 个**;引擎 raise 的类型只有两种内置类型:**ValueError 102 处** (facade 78/engine 13/persist 7/spi 2/meta 1/model 1)+ NotImplementedError 1 处 (meta.py:100,无参);引擎里 raise 运行时族类型(TypeError/AttributeError/KeyError/ IndexError/ArithmeticError/RuntimeError/OSError/JSONDecodeError…)**全部 0 次**。 ⇒ **java 那边是"约十处裸 RuntimeException 携带契约文案",本栈是 102 处裸 ValueError**, 一刀切把 ValueError 判内部的破坏面比 java 大一个量级 ⇒ 第 4 条**必须给 ValueError 开例外**。 - INTERNAL_FAILURE_MSG = "流程处理失败"(与 java **同名同值**,八栈可 grep 互查;逐字未改措辞)。 - is_foreign_detail(exc_type, message, cause, trace) —— **四入参签名逐位对齐 java 的 isForeignDetail(type,message,cause,trace)**;命名取其 snake_case 直译,与本栈既有 normalize_actors/require_present_id 同风格且跨栈 grep 一眼对得上。 配套 runtime_internal_detail(≈ java jvmInternal,**多收一个 message 入参**:python 没有 NumberFormatException 这个独立类型,数字解析腿只能按 CPython 内置文案模板判)、 thrown_inside_engine(**与 java 同名**)、以及 python 专有的**抽取层** foreign_detail_of(exc) (零判据逻辑,只从活异常拆 (type, str(exc), __cause__ or __context__, reversed(extract_tb)); java 不需要它,因为 getClass()/getMessage()/getCause()/getStackTrace() 本就是分开的调用)。 - 第 4 条三腿互补,逐条都有测试钉:a 腿类型族**不含 ValueError**,只收它的两个非契约子类 UnicodeError/json.JSONDecodeError(引擎零处使用,普查已证);b 腿用 \A 锚定 CPython 数字/时间解析文案模板(could not convert string to float/invalid literal for int() with base N/ Invalid isoformat string/time data … does not match format/unconverted data remains 等), **模板不写死**——由 test_criterion_4_numeric_parse_templates 当场把 float('x')/int('x')/strptime 调炸取真实文案来钉,CPython 换版本漂了当场红而不是静默绿;c 腿 __module__ != "builtins" ⇒ 内部 (**这一腿是本栈能在"核心零依赖"前提下认全 DB 驱动异常的唯一办法**:aiomysql/asyncpg 都是 optional-extra、TYPE_CHECKING 惰性引用,驱动异常全都只 extends Exception、类型名零共性、 不能 import 来 isinstance)。 - 第 5 条用 traceback.extract_tb 的帧归属(抛出点在 jeeflow/ 内 ⇒ 透出)。 ⚠️ 关键发现:repository/base.py:224 的 except BaseException: rollback; raise **裸重抛会把 base.py 压成栈顶帧** ⇒ 第 5 条会误判成"引擎写的",只有第 4c 腿能兜住驱动原文—— **这一腿就是 c 腿存在的理由**,专门加了 test_criterion_4_driver_and_third_party_types_are_internal 把栈顶帧**故意钉在 jeeflow/repository/base.py** 来验它。 - 顶层出口:命中 ⇒ _log.error("[jeeflow] action 执行失败: action=%s", action, exc_info=e) + 返回固定文案;未命中 ⇒ self._error(str(e))。原文只进 logging(exc_info=e ⇒ 完整异常对象 +栈+它自己的 __cause__)与错误对象的 __cause__,msg/data 一律不含原文。 - 另加一枚**前提回归钉** test_engine_module_never_defines_its_own_exception_type:用 ast 扫全仓 ① 零自定义异常类 ② raise 的类型名集合 ⊆ {ValueError, NotImplementedError} ③ ValueError 载体数 ≥ 100。哪天有人给引擎加了契约异常类型(第 2 条就需要落地)或换了载体类型 (第 4 条例外的前提就废了、RuntimeError 契约腿会被静默mask),这一格会红并把处置写在断言消息里。 用 AST 而非 grep ⇒ 本轮新增的注释文本不会污染计数。 **按 owner 2026-10-02 裁定改了一格既有测试的读法(本轮唯一一处,红线要求先停下回报、已回报并获批)**: tests/spec_test.py:7022 test_i142_62_custom_handler_own_exception_still_propagates 改前断言 r2["code"] != 0 and "handler-boom" in str(r2["msg"])——后半钉的正是 §2.12 要挡的形状 (**集成方自己写的 custom handler 抛的原文**),它 :7019 的注释还写着"顶层 catch 把消息原样送出", 等于把泄漏机制当成了判据的一部分。改成两条独立断言:code != 0(**本格的真实意图**: 处理器自身炸不得被吞成成功、不许顺手降级成"记日志继续"把数据写脏)+ msg == "流程处理失败"。同格 :7016 的**引擎直用腿**(pytest.raises(RuntimeError, match="handler-boom")) 打的是引擎 API 不是门面出口、不受 §2.12 约束,**原样保留** ⇒ 这是**改读法不是改判据**。 铁证:变异 M1(出口还原 str(e))下这一格是**唯一变绿**的既有测试 ⇒ 它本就是一枚钉住泄漏的测试。 java 基准同形状也出固定文案(isForeignDetail 第 5 条:抛出点 com.example.Boom 不在引擎主包), 而 java 无测试钉它 ⇒ 这本就是一处未立案的跨栈分叉,本轮由 python 侧收口。 新增 tests/test_facade_internal_error_no_leak.py **25 格 / 809 行**:A 组 12 格纯函数 (五条判据各自可测 + **102 处契约文案反闸** test_criterion_4_never_misfires_on_engine_contract_value_error 用 57 条真实字面量 × 8 个模块帧逐条断言判「透出」 + AST 前提回归钉);B 组 13 格走**门面真实调用路径** (9 负向泄漏:AttributeError/TypeError/KeyError/数字解析 ValueError/JSONDecodeError/裸包装/ sqlite3 驱动错经 bizData 腿/第三方 provider/空 message;4 正向与解析腿:三种坏内容 ⇒ msg 逐字 读取流程定义 JSON 失败(**既不是解析器原文也不是 流程处理失败**)且门面零 ERROR 日志、 三条解析腿同句、__cause__ 是 JSONDecodeError)。 ⇒ 日志与 __cause__ 两半都有断言,防"把原文整个丢掉"也能绿的假修。 改前红样六组(tmp/137-3-1-python/mutation-137-3-1-python.py,字节级 shutil.copy2 备份 → CRLF 感知的字节级替换(锚点唯一性预检,命中数 ≠ 1 即中止)→ 跑 pytest → copyback → sha256 核对; 交付件 sha256 741e320f…,六组之后均还原逐字一致): · M1 出口还原 self._error(str(e))、判别式整个拿掉 ⇒ **9 failed**,全是我新写的泄漏侧格; **纯函数 A 组全绿**(M1 拆的是"接线"不是"判据")⇒ 这正是 spec §2.12 要求"走真实调用路径、 不只测纯函数"的实证:只写 A 组的话 M1 会全绿; · M2 判据收窄成"一律固定文案"(foreign_detail_of 恒 True)⇒ **36 failed**,含 **31 格既有测试** (test_i134_*×4、test_i139_*×2、test_i142_*×5、雪花 id 精度守卫、test_withdraw_*×2、 test_transfer_negative_matrix、test_guard_order_is_fixed_across_stacks 等) ⇒ "判据过宽=静默改契约面"在本栈不是理论风险,102 处 ValueError 契约文案背后有一整片既有测试看着 (java 侧这一片一处都没钉,本栈现在钉住了); · M3 一刀切把 ValueError 判内部(="只透契约异常族"在本栈的退化形)⇒ **40 failed**; · M4 删第 3 条 ⇒ 4 failed;M5 删第 4c 腿 ⇒ 2 failed;M6 删第 5 条 ⇒ 3 failed (三组各自只红对应那几条判据的格 ⇒ 五条互不顶替、各有独立覆盖面)。 读数(均我复跑非转述):PYTHONIOENCODING=utf-8 .venv/Scripts/python.exe -m pytest -q ⇒ **325 passed / 0 failed**(改前基线 **300**,300+25=325;含改读法那格转绿); 新格 **25 passed**;-k i142_62 **2 passed**。jeeflow/facade.py 行尾保持全 CRLF (改后 2020 CRLF / 0 bare LF),numstat 190 3 与 12 3,非整文件重写。 jeeflow/spi.py(§3-6 06a8d07)零改动;build/lib/、.venv/、__pycache__/ 未碰。 ⚠️ 四条如实记档(都不是缺陷,但下轮别当新问题重开): ① RuntimeError 在类型族里,而 NotImplementedError 是它的子类 ⇒ 引擎那唯一一处 raise NotImplementedError(meta.py:100,**无参**)会被第 1 条(message 为空)先挡掉; 改后出固定文案,比改前出**空 msg**(违反 §2.1「msg 为失败原因」)更好。 AssertionError 同理留在族里:引擎 assert x, "文案" 实数 **0 处**,不会误伤。 两处的安全性都由上面那枚 AST 回归钉看着(谁给引擎加 RuntimeError 契约腿它当场红)。 ② _FOREIGN_VALUE_ERROR_RE 的模板与 CPython 版本文案绑定(本机 3.11.15,requires-python >=3.10)。 失败模式是**响的**(测试当场调炸内置函数取真实文案再断言),但真漂了要同步更新正则。 ③ **新增了一处下游可见输出**:内部异常现在会在 logger jeeflow.facade 上出一条 ERROR(带完整 traceback)。 集成方若未配 root handler,Python 的 lastResort 会把它打到 stderr。与 java 的 log.log(Level.SEVERE, …, e) 同形、也与本仓 engine.py 既有 logging.exception(...) 惯例一致, 但**建议进发版说明**。 ④ 第 2 条在 python 是**文档化的空档**(本栈无契约异常类型):保留了注释位与八栈逐条可比对的编号, 并加了 AST 回归钉——一旦有人引入契约异常类型,那格会红并提示要落地第 2 条。 ⑤ 走的是 ① 出口层判别式(与 java 基准一致),没去改 moon 那种 ② 构造层;解析腿的 ② (issues/139 的 raise ... from e)原样保留——两者互补:② 让契约文案能逐字透出, ① 兜住任何形状的外来原文(含第三方 provider 与 base.py:224 的裸重抛)。 ⑥ processInstance/bizData 的 meta_reader 调用是**同步**的(return self._meta_reader.read_by_…(...), 无 await)——既有形状,与 §3-1 无关,按原样写测试桩;若那是缺陷需另立案。 | 23 小时前 | |
chore(eol): 标 *.sql text eol=lf——建表 SQL 副本的行尾不再随维护机 core.autocrlf 漂移 副本由 jeeflow-hub/scripts/sync-schema.sh 从 java 唯一编辑源分发,blob 本就是 LF,但本仓没标 *.sql:Windows 维护机上检出成 CRLF,脚本分发 LF 后 git 逐字比对即报"已修改"(内容逐字相同,纯行尾噪声)。 标上 eol=lf 后工作树与 blob 一致,任何平台跑完分发都零脏项。 | 8 天前 | |
chore(publish): 发布准备 1.0.0(packages.find 限定 jeeflow,extras mysql/postgres) | 2 个月前 | |
refactor(flows): 流程定义解耦 java 仓,本仓自带 flows/ 副本 + 执行期精确镜像 - 新增 flows/(15 个 JSON 入库副本)+ flows_resolver.py(仓根 resolver,pyproject 只发 jeeflow*,不打包) - 读路径只认本仓 flows/;java 兄弟目录存在时执行期全量镜像(复制+删孤儿) - demo/main.py + 4 处测试改用 flows_resolver.dir();Dockerfile.demo/CI 改单仓上下文 - docs 同步(15 个 + resolver 说明 + 会签段 issues/91 回灌) - 单语言用户下载即用,无需 checkout jeeflow-java | 1 个月前 | |
chore: 补 Apache-2.0 LICENSE(对齐上游 mldong 框架);MIT 残留同步 | 2 个月前 | |
docs: 补 NOTICE(Copyright mldong),README/pyproject 对齐 Apache-2.0(去掉版本绑定) Co-authored-by: Cursor <cursoragent@cursor.com> | 1 个月前 | |
docs: 补 NOTICE(Copyright mldong),README/pyproject 对齐 Apache-2.0(去掉版本绑定) Co-authored-by: Cursor <cursoragent@cursor.com> | 1 个月前 | |
refactor(flows): 流程定义解耦 java 仓,本仓自带 flows/ 副本 + 执行期精确镜像 - 新增 flows/(15 个 JSON 入库副本)+ flows_resolver.py(仓根 resolver,pyproject 只发 jeeflow*,不打包) - 读路径只认本仓 flows/;java 兄弟目录存在时执行期全量镜像(复制+删孤儿) - demo/main.py + 4 处测试改用 flows_resolver.dir();Dockerfile.demo/CI 改单仓上下文 - docs 同步(15 个 + resolver 说明 + 会签段 issues/91 回灌) - 单语言用户下载即用,无需 checkout jeeflow-java | 1 个月前 | |
chore: bump version to 1.8.37 | 17 小时前 |
jeeflow · Python
轻量级异步工作流引擎 — Python 实现,对齐 jeeflow SPEC。
统一门面 JeeflowFacade.flow(action, dict);支持管理扩展(流程设计/历史/委托)、
assignee 变量解析与 flow.auto/flow.admin 系统代执行。
快速开始
pip install jeeflow
# 启动 demo
cd demo
python main.py
# → http://localhost:8100
项目结构
jeeflow/
├── model.py # 域类型 + LogicFlow JSON 解析
├── spi.py # SPI 接口(ProcessRepository / UserProvider / IDGenerator / ExpressionEvaluator)
├── engine.py # 引擎核心(EngineImpl)
├── extensions.py # 扩展体系(拦截器 / 事件 / HandlerRegistry)
├── memory.py # 内存仓储(MemoryRepository)
├── repository/ # JDBC 多库实现(base 共享核心 + mysql/postgres 适配器)
├── __init__.py
tests/
├── spec_test.py # 10 项 SPEC 合规测试
demo/
├── main.py # FastAPI 演示站
├── web/
│ └── index.html # 前端仪表盘
pyproject.toml # 项目配置
引擎使用
from jeeflow import EngineImpl, MemoryRepository
from jeeflow.model import parse_flow_model
import json
repo = MemoryRepository()
engine = EngineImpl(repo)
# 加载流程定义(LogicFlow JSON)
with open("flow.json") as f:
flow = parse_flow_model(json.load(f))
# 启动流程
inst = await engine.start_process_instance_by_id(define_id, "user1", {"amount": 3000})
# 完成任务
inst = await engine.execute_process_task(task_id, "user1")
# 驳回
inst = await engine.execute_and_jump_to_end(task_id, "user1")
# 跳转
inst = await engine.execute_and_jump_task(task_id, "user1", target_task_name="previous")
扩展
from jeeflow import EngineExtensions, FlowInterceptor, HandlerRegistry, EventType
class MyInterceptor(FlowInterceptor):
async def pre_handle(self, node, inst): return True
async def post_handle(self, node, inst): pass
@property
def order(self): return 10
async def on_event(evt):
print(f"[{evt.type.value}] instance={evt.instanceId}")
engine.set_extensions(EngineExtensions(
interceptors=[MyInterceptor()],
event_listener=on_event,
registry=HandlerRegistry(),
))
SPI 接口
| 接口 | 说明 | 必须 |
|---|---|---|
ProcessRepository |
流程数据持久化 | ✅ |
UserProvider |
用户信息获取 | 可选 |
IDGenerator |
ID 生成(默认时间戳) | 可选 |
ExpressionEvaluator |
决策表达式求值 | 可选 |
运行测试
pip install pytest pytest-asyncio
pytest tests/ -v
# 10 passed
多语言联邦
与 Java / Go / Node / PHP / Rust 等实现共享同一套契约与流程 JSON,详见 jeeflow-doc。
License
Copyright © 2025-2026 mldong
Licensed under the Apache License, Version 2.0. See LICENSE and NOTICE.