58 KiB
| name | version | description | trigger_conditions | ||||
|---|---|---|---|---|---|---|---|
| module-development-spec | 1.0.0 | 开发「业务模块」(Python 包)时必读——模块目录结构(包目录=模块名,非 src)、模块无独立 app.py/端口/Dockerfile/build.sh(那些是应用级的,模块带 app.py 就是错的)、取库名用 get_module_dbname 禁止硬编码 DBNAME、models/json/dspy 四段式。开发「应用脚手架」(app/{应用名}.py + conf/config.json + build.sh)时不要用本技能,改用 web-application-spec。 |
|
Work Logs / Delivery Archive
For production module/application development, update a dated work log (use the REAL current system date; late-night follow-up fixes go under their real date or are labeled next-day follow-ups) before finishing, e.g. docs/work-log-YYYY-MM-DD.md. Entry should include: scope/background + repo/module name; timeline/commit list; key technical decisions and pitfalls; verification performed (incl. environment-limited checks that couldn't run); current branch/commit state. Do NOT mark a task done without this archive when deliverables must be retained.
This skill defines the complete workflow for standardized modules: ahserver ecosystem + bricks-framework frontend + sqlor database backend.
Module Philosophy — Host-Agnostic
- A module is a self-contained unit ANY host can load (pipeline-app / sage / future apps). It depends ONLY on: ① foundation packages (sqlor, ahserver ServerEnv, appPublic utilities); ② its own data tables (or shared tables like sage's appcodes); ③ other modules it explicitly imports.
- It does NOT depend on any host entry point, host-specific config files, or host wwwroot paths.
load_{module}()is the ONLY integration point — it registers functions to ServerEnv; the host decides how to wire it in.- A module is NOT an independently deployable unit. It has NO standalone
app.pyentry point, NO own port, NO own Dockerfile/service, NO own deploy script. It only runs because a host application callsload_{module}(). A module that carries its ownapp.py/ port / deploy script is wrongly built — fix the module, don't deploy it separately. - Interaction-layer modules (e.g. pipeline-task) have NO data tables — pure thin wrappers calling other modules' functions via ServerEnv; they provide .dspy + .ui only.
模块本地仓库位置
在 pipeline 产线机构工作空间中,模块本地仓库在机构 modules/{模块名}/(见 project-directory-spec)。模块是应用内的功能模块(Python 包),不是独立部署单元。
部署测试前必须给模块仓库设置远程仓库(git remote add origin <远程地址>),否则部署时无法 git pull 拉取最新代码。
Directory Structure
mymodule/ # module root (replace with actual name)
├── mymodule/ # Python package: __init__.py (required), init.py, *.py
├── wwwroot/ # frontend: .ui / .dspy / .js / .css
├── models/ # {tablename}.json database table definitions
├── json/ # {alias}.json CRUD operation definitions
├── init/ # data.json / data.yaml initial data
├── scripts/ # supporting scripts (load_path.py RBAC registration)
├── skill/SKILL.md # MANDATORY agent-facing spec (data model, endpoints, pitfalls)
├── pyproject.toml # Python packaging
└── README.md # module documentation
Core Implementation Requirements
1. Module Initialization (init.py)
- Purpose: register ALL module functions with ServerEnv so .ui/.dspy can call them directly. NOT for route registration (wwwroot files auto-routed via
/{module_name}/filename.ext). - CRITICAL: export functions in
__init__.py— all async functions defined in init.py MUST be imported in the package__init__.py; otherwise dspy calls failNameError: name 'xxx' is not defined. - Pitfall: Triple-place function registration — adding/removing a function requires updating THREE files in sync: ① implementation (
mymodule/mymodule.py); ②__init__.pyimport line; ③init.pyenv.xxx = xxxinload_{module}(). Missing ② → ImportError/AttributeError at init; missing ③ → NameError in .dspy/.ui; removed from ① only → ImportError at load. Cleanup check:grep -rn 'function_name' mymodule/ --include='*.py'must hit exactly those three places. - Pitfall: CRUD dspy wrappers use PLURAL table names — xls2ui generates wrappers (
add_suppliers.dspy) callingcreate_suppliers/update_suppliers/delete_suppliers, while init.py usually defines singular names (create_supplier). Register BOTH:env.create_supplier = create_supplier; env.create_suppliers = create_supplier(same for update/delete). Symptom: CRUD "add" returns 500function not found. - Pitfall: Subagent delegation for module creation — ALWAYS validate after (subagents consistently err even with correct specs; observed 2/3 failure rate). Consistent errors: ① init.py/init.py misplaced at module ROOT instead of package dir; ② model JSON
{"table":...,"fields":{...}}instead of{"summary":[...],"fields":[...],"indexes":[...],"codes":[...]}; ③ CRUD JSON{"table":...,"list":{...}}instead of{"tblname":...,"params":{"browserfields":{...},"editable":{...}}}; ④ hallucinated sqlor APIs (sqlor.save/list/one/delete/insert/queryDO NOT EXIST — onlysor.C/U/D/R/I/sqlExe). Mandatory validation: move misplaced files to package dir; check model JSONs havesummary(array primary); check CRUD JSONs havetblname+params.editable; grep for fake sqlor APIs → fix to sor.C/U/D; dspy audit (no imports/print/uuid); py_compile all .py. Seereferences/bulk-module-creation-pattern.md"Subagent Validation Checklist". - Pitfall:
debug()in .dspy has NO filename — logs show[sage][debug][<string>:N](framework injects dspy code into a<string>context). Always prefix manually:debug(f'product_category_create.dspy: START params_kw={dict(params_kw)}')— essential when several dspy run in sequence (add → refresh). - Pitfall: DSPY API handlers must forward ALL client params — never hardcode dispatch fields — e.g. read
task_type = params_kw.get('task_type', 'separate')and forwardoutput_dirif present; do NOT build{'task_type': 'separate', ...}. Symptom: client sendsseparate_fullbut worker log showsseparate; hours to trace because the worker is correct — the DSPY silently drops the param. Audit: grep API dspy for hardcoded'task_type'/'mode'dispatch strings inside payload construction. - Pitfall: py_compile is INVALID for .dspy files — dspy is injected into an async function at runtime (top-level
return/awaitare correct); py_compile gives false positives ('return' outside function). Use py_compile only for .py; validate .dspy with the dspy audit script (grep-based). - CRITICAL: dspy MUST use explicit
return— ahserver wraps code inasync def myfunc(request, **ns):and awaits it; a bareresultexpression returns None. Seereferences/dspy-execution-and-module-structure.md(execution model, pre-loaded globals, Tree widget data format, module symlink patterns). - Pitfall: Sage startup requires environment variables (e.g.
ALIPAY_PUB) read at import time inapp/sage.py— cannot full-start locally (FileNotFoundError). Validate with py_compile (.py) + dspy audit instead of startup. - Pitfall: Raw SQL column names — ALWAYS verify against models/{table}.json, never guess. Check first:
python3 -c "import json; cols=[f['name'] for f in json.load(open('models/table.json'))['fields']]; print(cols)". Known wrong guesses → correct: permissionpermcode/permname→path/name; userscreated_date→created_at; organizationorg_name→orgname. - Pitfall:
{{id}}in uapi response templates resolves to Python built-inid()— renders<built-in function id>instead of the field. NEVER use{{id}}; map to a different name:'{"taskid":"{{taskid}}","status":"{{status}}"}'. Affects any uapi whose upstream response has anidfield. - Pitfall: uapiio
input_fieldsMUST be a JSON array — bricks.js LlmIO callsthis.input_fields.forEach(...); an object{"field":{...}}throwsTypeError: this.input_fields.forEach is not a function. Use array form:[{"name":"prompt","label":"用户输入","uitype":"text","required":true}]. Canonical reference: existingktv_asr_transcribe_iorecord. - Pitfall:
json.dumps()in uapi data templates →data=None—{{json.dumps(prompt, ensure_ascii=False)}}can render silently to None (logsbody=None). Use plain string interpolation"{{prompt}}"and HARDCODE the upstream model ID ("model":"hy-image-v3.0"), don't pass{{model}}. Source real model IDs from provider/v3/models(ARK) or/v1/models(tokenhub). - Pitfall:
returninsideasync with→ silent NoneType — returning insideasync with db.sqlorContext(...) as sor:returns None →return data type error, <class 'NoneType'>. Collect results inside the block,returnAFTER it exits. Minimal repro:return {'ok': True}inside async with also fails. - Pitfall:
sor.C()silently drops records whencreated_atmissing — sqlor does NOT auto-set timestamps; withcreated_at TIMESTAMP NOT NULLthe insert silently vanishes. Always setns['created_at'] = curDateString()(andns['org_id'] = (await get_userorgid()) or '0'). Symptom: dspy returns{"success": true}but SELECT returns zero rows. - Pitfall: Bash heredoc
\$escapes corrupt sqlor placeholders —cat > f.dspy << 'EOF'with\${pid}\$writes the backslash literally; sqlor fails silently on the unrecognized placeholder. Never escape$in single-quoted heredocs — write${pid}$as-is. Verify:grep 'project_id' file.dspy | cat -Amust show no\$. - Pitfall: Do NOT name module functions
get_module_dbname— that name is already a ServerEnv-provided global used by .dspy; registering it overwrites the global. Use a private prefix (_get_dbname()).
2. Frontend Development
- Mandatory: bricks-framework for all frontend; .ui files as PURE JSON (NOT HTML/CSS) in wwwroot/; auto-routed
/{module_name}/filename.ext— no manual route registration. Seereferences/bricks-ui-pitfalls.md(id at widget level not options, Button click, Popup/Form patterns, script actiontype Jinja2 limits). - ahserver auto-serves .css/.js from wwwroot/ — DO NOT add
<link>/<script>tags in .ui files (common mistake → unnecessary 403 with RBAC). BUT auto-served files are STILL subject to RBAC: every .css/.js MUST be registered in load_path.py; pre-auth files (theme CSS) useanyrole. - build.sh links module wwwroot into the main app's wwwroot.
2.0 Module Entry Point (index.ui) — MANDATORY
- Every business module MUST have
wwwroot/index.ui(exempt: foundation/reference modules rbac, appbase, accounting, apppublic, sqlor, ahserver). It integrates ALL .ui/.dspy into one navigation page: ResponsableBox with clickable card widgets (VBox + binds click → urlwidget) per feature; default content area VBox withid: "app.<module>_content"for urlwidget targets. - ALL url values MUST use
{{entire_url('filename.ui')}}/{{entire_url('api/xxx.dspy')}}(converts server-relative paths to the correct runtime URL prefix). Pattern:
{"widgettype":"VBox","options":{"width":"100%","height":"100%","padding":"20px"},"subwidgets":[
{"widgettype":"Text","options":{"label":"模块名称","fontSize":"24px"}},
{"widgettype":"ResponsableBox","options":{"gap":"16px","minWidth":"250px"},"subwidgets":[
{"widgettype":"VBox","options":{"backgroundColor":"#FFFFFF","padding":"20px","cursor":"pointer"},
"binds":[{"wid":"self","event":"click","actiontype":"urlwidget","target":"app.module_content",
"options":{"url":"{{entire_url('feature_page.ui')}}"},"mode":"replace"}],
"subwidgets":[{"widgettype":"Text","options":{"label":"功能名称"}}]}]},
{"widgettype":"VBox","id":"module_content","options":{"width":"100%","flex":"1","marginTop":"20px"}}]}
2.1 URL Path Rules — MANDATORY
- ALL url fields (Menu items, Button url, Form url, Iframe url, urlwidget targets, any url property) MUST use
{{entire_url(url)}}. WRONG:"url": "/module/page.ui"or"url": "feature.ui". CORRECT:"url": "{{entire_url('/module/page.ui')}}". - Do NOT wrap:
data:URIs,#anchors,http(s)://URLs,${var}variable references.
2.2 JSON CRUD File URL Rules — MANDATORY
- CRUD definitions in
json/referencing SAME-module aliases MUST use../prefix:"url": "{{entire_url('../handover_items_list')}}"(json/ is a subdir; the .dspy CRUD files and generated .ui live at wwwroot root, so URLs must step out of json/). Applies tosubtables[].url,browserfieldsdataurl, etc. Cross-module aliases (e.g.appcodes_list) need NO../— resolved via inter-module routing.
2.2a CRUD new_data_url — Do NOT Pass editexclouded Fields as Query Params
- If
new_data_urlpasses a field as query param (?supplier_id=...) that is also ineditexclouded, form POST body + URL params merge intoparams_kwas a LIST →sor.Cgets a list → MySQLOperand should contain 1 column(s). Fix: remove the query param fromnew_data_url; if the field must be pre-set (subtable context), add it as a hidden form field instead. CORRECT:"new_data_url": "{{entire_url('../api/create.dspy')}}".
2.3 JSON CRUD editable Section — MANDATORY
- All DataViewer/Tabular list JSON configs MUST include an
editablesegment, else 新增/编辑/删除 buttons can't submit. It defines three target URLs pointing atwwwroot/api/.dspy scripts. new_data_url/update_data_url/delete_data_urlMUST be atparamsTOP LEVEL — NOT nested insideeditable. xls2ddl reads them from params top level ({% if new_data_url %}); nesting silently generates the defaultadd_{tablename}.dspy, bypassing any custom create logic. Real case (2026-07-13): discount module nested them →created_bynever set by custom handler; fixed by moving to params top level with/module/api/absolute paths.wwwrootis the web root and invisible in URLs — paths must NOT containwwwroot/; from json/ context use../api/→"new_data_url": "{{entire_url('/module/api/tablename_create.dspy')}}"etc.- The three .dspy files (
xxx_create.dspy,xxx_update.dspy,xxx_delete.dspy) MUST exist inwwwroot/api/. Missingeditable→build_add_form()/build_update_form()fail on undefinedthis.editable.new_data_url._edit.json(form edit page) needs editable too. Templates:references/json-config-pattern.md,references/crud-json-toolbar-bind-pattern.md.
2.3b bricks Toolbar Bind — Only 5 Valid Actiontypes
- Valid
actiontype:urlwidget,script,url,datawidget,event.dspyandfunctionare NOT valid. Toolbar → dspy pattern:{"actiontype":"urlwidget","target":"PopupWindow","popup_options":{"title":"标题","height":"200px","width":"400px"},"params_mapping":{"mapping":{"id":"target_param"},"need_other":false},"options":{"method":"POST","url":"{{entire_url('../api/xxx.dspy')}}"}}— dspy return renders in the PopupWindow; if it returns a DataViewer widget, the url must carry_webbricks_=1. - 禁止:
fetch()/bricks_fetch()/ setInterval insidescriptactiontype — bricks explicitly forbids fetch/setInterval in script.
2.3c Module-Internal entire_url() Must Use Absolute /module/ Paths
entire_url('supply_contracts_list')may resolve WITHOUT the module prefix (/supply_contracts_listinstead of/supplychain/supply_contracts_list) → RBAC 403. Use{{entire_url('/supplychain/supply_contracts_list')}}in index.ui/menu.ui. Check:grep "entire_url(" wwwroot/index.ui wwwroot/menu.ui | grep -v '/supplychain/'.
2.3d dspy-Returned DataViewer URL Must Be Absolute
- dspy has NO
entire_url()(Jinja2-only). Return an absolute path in the widget:'url': f'/supplychain/supply_contracts_list?supplier_id={supplier_id}&_webbricks_=1'(relative../...resolves wrong inside PopupWindow context).
2.4 Menu Widget — Do NOT use binds
- Menu handles clicks internally — no extra binds for itemclick. Just set
url: "{{entire_url('page.ui')}}"on items.
2.5 DSPY File Guidelines
- ahserver supports GET and POST (framework distinguishes). ALL params (query string AND POST body; nested JSON objects/arrays preserved) auto-parsed into
params_kw(dict-like):params_kw.get('key')/params_kw.key. Do NOT read POST viaos.environ/os.read()/sys.stdin. - Return consistent JSON:
status,data, optionalmessage/total. Validate inputs; call business logic from init.py-registered functions. - Logging: use
debug_params('name', dict_obj)instead ofdebug(f'{dict_obj=}')(dumps whole objects). To add a new global available in ALL .dspy, inject viaself.y_envinahserver/processorResource.py(seereferences/web-app-and-dspy-spec.md).
CRITICAL: ServerEnv() is NOT needed in .dspy scripts
- DO NOT write
env = ServerEnv()in .dspy. All ServerEnv-registered functions are direct globals:get_module_dbname(),get_user(),get_userorgid(),DBPools(),password_encode(),getID(), module functions likecreate_user_apikey. CORRECT:dbname = get_module_dbname('dapi');org_id = (await get_userorgid()) or '0'.ServerEnv()only in .py module files. - Pitfall:
ServerEnv()is a process-level singleton — every call returns the SAME instance;env.get_module_dbname = ...set anywhere is visible everywhere. Do NOT assume a new instance per call. - Pitfall:
ServerEnv()≠request._run_ns— NEVER useServerEnv()for per-request user/session context.get_user()/get_userid()/get_userorgid()are registered onrequest._run_ns(a.k.a.self.y_env, a plain DictObject set in processorResource.py:347-349), NOT on the singleton —await env.get_user()silently returns None → created_by/org_id NULL. CORRECT in .py:env = request._run_ns; user_id = await env.get_user().ServerEnv()is correct only inload_{module}()(startup) and for startup-time globals with no request. Detection:grep -n 'env = ServerEnv()' module/init.pyand check the next 6 lines for get_user/get_userorgid/get_userid. Real case (2026-07-13): discountcreate_marketing→ created_by always NULL, 7 functions affected (siblinggenerate_promo_codecorrectly usedrequest._run_ns). - Pitfall: Module pkgs go under the HOST app's
pkgs/, not cross-applications — e.g. tenant module at/d/apitest/sage/pkgs/tenant/with symlinksage/wwwroot/tenant -> ../pkgs/tenant/wwwroot; NOT under/d/apitest/pipeline-app/pkgs/tenant/. Each application installs modules into its OWNpkgs/— cross-app placement breaks module independence.
sqlor method argument counts — sor.I is special
| Method | Args | Pattern |
|---|---|---|
| sor.C / sor.R / sor.U / sor.D | 2 | sor.C('table', ns) |
| sor.I | 1 | sor.I(ns) — table name resolved from the sqlor context |
sor.I('table', data) → TypeError: I() takes 2 positional arguments but 3 were given.
Read-Only / Dashboard Modules
Not all modules need tables/CRUD — models/ and json/ can be omitted entirely. Preferred pattern: load_XX.py exposes async data functions via ServerEnv → individual .ui files use Jinja2 {{get_data(request)}} → RefreshWidget wraps each card for auto-refresh (NO .dspy files or JS polling). See references/read-only-module-pattern.md: Jinja2+RefreshWidget architecture (preferred), legacy .dspy+JS polling (deprecated), ECharts ChartBar/ChartLine with inline data or data_url, cross-table queries via the shared sage database, concurrent-user detection.
Database Integration
- Mandatory: use sqlor-database-module for all relational operations. Table definitions as
{tablename}.jsonin models/; CRUD definitions as{tablename_or_alias}.jsonin json/. - Bulk xlsx→json conversion:
scripts/xlsx2json_models.pyconverts all.xlsxmodel files across Sage modules to JSON in one pass. - CRITICAL: 模块取库名禁止硬编码 DBNAME —— 库名由宿主应用决定,不是模块自己写死。模块取库名统一用
get_module_dbname('模块名')(.dspy 里直接调用,是 ServerEnv 注入的全局,无需 import)或ServerEnv().get_module_dbname('模块名')(.py 模块文件里)。禁止在模块里写DBNAME = 'hrs6'/DBNAME = 'sage'/dbname = 'xxx'之类硬编码——同一模块可被多个应用挂载,各应用的库名不同,硬编码会查错库或 table-not-found。宿主应用在app/{appname}.py里定义get_module_dbname(m)并挂到 ServerEnv(见 web-application-spec)。
4. Initialization Data (init/data.json or init/data.yaml — three formats)
-
硬性约束:init/data.json 必须是合法 JSON 的真实种子数据,禁止占位符文本。写「8 组 appcodes 编码种子」这类纯描述文字(不是 JSON)会导致部署时
json.load失败(JSONDecodeError)→ 建表/插种子中断 → 应用起不来。每个模块的 init 数据必须写完整真实数据(appcodes 组 + 每个 k/v),不能只写「应该有几组」的描述。交付前用python3 -c "import json;json.load(open('init/data.json'))"自验 JSON 合法性。 -
Format A — Direct table seeding (JSON):
{"table1": [{"field1":"value1", ...}, ...]} -
Format B — Appcodes registration (JSON):
{"appcodes":[{"parentid":"sc_relation_type","parentname":"供销关系合作类型","items":[{"k":"distribution","v":"分销"},{"k":"agency","v":"代理"}]}]} -
Format C — Appcodes registration (YAML, used by accounting etc.):
appcodes:entries use{id, name, hierarchy_flg};appcodes_kv:entries are FLAT records{id, parentid, k, v}(not nested under items).parentidin appcodes_kv must match anappcodes.id. When adding a code group, add BOTH the appcodes parent AND all appcodes_kv children. -
Pitfall: parentid too long → appcodes_kv.id exceeds VARCHAR(32) — the loader generates
id = {parentid}_{k}; bothidandparentidareVARCHAR(32).supplier_settlement_cycle(27) +quarterly(9) = 37 chars →DataError: Data too long for column 'id'. Rule:len(parentid) + 1 + len(longest_k) ≤ 32→ keep parentids ≤ 22 chars. If already referenced inmodels/*.jsoncodescond, update BOTH init/data.json and all referencing model files. -
Pitfall: YAML
hierarchy_flg:0fullwidth colon — pre-existing quirk in some data.yaml files (fullwidth:instead of ASCII:). Do NOT "fix" unless the user asks — it may break the loader expecting the original format.
5. Encoding Management
- Encodings live in appbase tables:
appcodes(id str(32) PK, name, hierarchy_flg str(1) — '0'=single-level, '1'=multi-level);appcodes_kv(id str(32) PK, parentid str(32), k str(32), v str(255)).
5b. Codes Referencing Future Modules
- Even when a field references a module that doesn't exist yet (e.g.
productid→ futureproducts), STILL add the codes entry now:{"codes":[{"field":"productid","table":"products","valuefield":"id","textfield":"product_name"}]}. Safe — codes only affect UI dropdown rendering and are ignored if the table is missing. Do NOT defer until the referenced module exists.
pyproject.toml Configuration
- Package
nameMUST match the module directory exactly (e.g.name = "customer_management"). - Dependencies: only DIRECT code deps —
"sqlor"(NOT"sqlor-database-module"),"bricks_for_python"(NOT"bricks-framework"). Do NOT list foundation packages (ahserver,appbase,rbac,apppublic) — installed by build.sh separately, not on PyPI.pip install .must succeed locally without fetching non-PyPI packages.
[build-system]
requires = ["setuptools>=45", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "mymodule"
version = "1.0.0"
requires-python = ">=3.8"
dependencies = ["sqlor", "bricks_for_python"]
[tool.setuptools.packages.find]
where = ["."]
include = ["mymodule*"]
Project Files
- README.md: purpose, features, data tables, installation, integration in plain language — never empty or boilerplate-only.
- skill/SKILL.md (MANDATORY per module repo): YAML frontmatter (name, description) + markdown covering module architecture, data model (note when no tables), key DSPY endpoints, module-specific pitfalls. Concise — consumed by AI agents.
- build.sh: module build script integrated into the main application's build process.
Build Process Integration
build.sh processes ALL modules with models/ or json/: ① install xls2ddl; ② models/ with .xlsx → xls2ddl mysql . > mysql.ddl.sql; ③ models/ with .json → json2ddl mysql . > mysql.ddl.sql; ④ json/ → xls2ui -m ../models -o ../wwwroot ${modulename} *.json (generates CRUD UI); ⑤ symlink module wwwroot → main app wwwroot.
CRITICAL: Symlink Generated CRUD Directories
- xls2ui generates subdirectories per CRUD alias (
wwwroot/{alias}/with index.ui, get_/add_/update_/delete_*.dspy). These are NOT covered by individual-file symlink rules — link whole directories:ln -sf /path/to/module/wwwroot/{alias}/ .(in build.sh: auto-link all wwwroot subdirs exceptapi|styles|scripts). - Symptoms: CRUD pages 404 even though xls2ui ran (dir exists in module wwwroot but not linked);
processorResource.py: raise Exception(f'{str(request.url)=} invalid path')= URL path not in ahserver's route table ≈ file/dir not symlinked to deployment target's wwwroot.
CRITICAL: CRUD-Generated Directories Are READ-ONLY — Never Modify Files Inside
- Every file in
wwwroot/{alias}/(index.ui, get_/add_/update_/delete_*.dspy) is AUTO-GENERATED — never edit directly. WRONG: editing them; CORRECT: editjson/{alias}.json(CRUD definition) /models/{table}.json(table definition) → re-run build.sh (xls2ui regenerates and overwrites). - JSON manipulation safety: complex .ui files are error-prone to string insertion (one missing comma silently breaks the page). ALWAYS read with
json.load()→ modify dict → write withjson.dump(). - Pitfall: xls2ddl
data_new_tmpl{{summary[0].pkey}}→WHERE None = %s— model JSONs useprimary(an array), NOTpkey; Jinja2 renders the missing attr as None →OperationalError: (1054, Unknown column 'None' in 'WHERE')on EVERY add across all CRUD tables (xls2ddl commit fc91486, June 2026). Fix:{{summary[0].primary[0]}}inxls2ddl/xls2ddl/tmpls.pyline 308. - When json/ config cannot express needed logic: ① create custom dspy in
wwwroot/api/(e.g. api/add_user.dspy); ② pointnew_data_url/update_data_url/delete_data_urlat it; ③ register paths inscripts/load_path.py(RBAC) +sage/load_path.py(routing); ④ re-run xls2ui — generated index.ui uses the custom URLs. - Pitfall: bricks Tabular code fields submit values as LISTS — root cause: the field appears in BOTH
new_data_urlquery params ANDeditexclouded→ ahserver merges URL + body into a list. The correct fix is the JSON config (drop the duplicate query param), NOT list-unwrap workarounds in every dspy. - Common api/ dspy customizations: set defaults (
ns['created_at'] = curDateString()); auto-generate business codes likecontract_code(seereferences/custom-create-dspy-required-fields.md); clean_textsuffix fields before sor.U/C (Tabular sends these from code lookups); preserve password on update (pop empty password from ns);try/exceptwithformat_exc()+str(e)to show real errors to frontend. - CRITICAL: Model JSON changes require xls2ui rebuild — changing models/*.json (codes/fields/indexes) does NOT auto-update generated CRUD .ui. Re-run build.sh after ANY model change, else dropdowns/forms/views keep the old model. #1 cause of "I changed the model but the page still shows old data". See
references/model-change-rebuild-pitfalls.md(.xlsx/.json load order, get_code.dspy module.table resolution failure, collation mismatch). - Pitfall: post-rebuild THREE checks — ① files exist:
ls wwwroot/<module>/<alias>/index.ui(rebuild can fail silently); ② symlinks intact for root-level alias URLs:ls -la sage/wwwroot/<alias>→ should point tosage/wwwroot/<module>/<alias>; ③ RBAC refreshed: new index.ui has new dataurl/get_code.dspy calls → verify permissions exist AND restart Sage (RBAC cache has 10-min TTL; load_path.py inserts permissions but doesn't assign roleperm for existing ones). - Pitfall: xls2ui strips code-type fields from generated editexclouded — a field in BOTH
browserfields.alters(uitypecode+ dataurl) ANDeditexcloudedis stripped from generated index.ui's editexclouded → bricks renders a UiCode widget calling.build_options()on undefined. Symptom:TypeError: Cannot read properties of undefined (reading 'length')atbricks.UiCode.build_optionson Add/Edit. Fix: add missing fields back to the generated index.ui editexclouded (.gitignore'd →git add -f); the fix is undone if xls2ui re-runs, so deploy withgit pullonly, skipping build.sh. Long-term: fix xls2ui. Verification (per alias): compare sourcejson/{alias}.jsonparams.editexcloudedvs generatedwwwroot/{alias}/index.uirow_options.editexclouded. - Pitfall:
cheighton CRUD index.ui makes title row fill viewport — generated files contain"cheight":40on both outer VBox and Tabular options; remove ALLcheightproperties (Tabular sizes itself). Check:grep -rn 'cheight' wwwroot/*/index.ui.
CRITICAL: Do NOT Replace Framework-Generated Code with Custom Scripts
- CRUD framework endpoints are base functionality (handle RBAC,
logined_userorgid,confidential_fieldsredaction, DBFilter parsing). Do NOT hand-write replacements — e.g. creatingapi/llm_list.dspyto replace generatedget_llm.dspyjust for_textdisplay; instead fix thedataurlAPI to return[{field_name, field_name_text}]. Custom scripts often violate .dspy conventions and create maintenance burden. Propose custom scripts ONLY for genuine special requirements — and discuss the approach FIRST. Zero tolerance for unsolicited replacement of stable framework code.
CRITICAL: Hand-written list .dspy SHADOWS framework auto-generated endpoints
- If
json/{alias}_list.jsonexists, the framework auto-generateswwwroot/{alias}/get_{table}_list.dspy. A hand-writtenwwwroot/api/get_{table}_list.dspySHADOWS it → 500s (filters like org_id on missing fields), 403s (bypasses framework RBAC/logined_userorgid), silent data leakage (misses confidential_fields redaction). Rule: ifjson/{alias}_list.jsonexists, do NOT createapi/get_{table}_list.dspy. Hand-write only: custom business-logic endpoints (create/update/delete beyond simple CRUD) and client dropdown data APIs returning[{value, text}](NOT list endpoints). Detection: list endpoint 500/403 + both files exist → delete the hand-written one.
CRITICAL: CRUD-Generated wwwroot Directories Must NOT Be Git-Tracked
- They are build artifacts, regenerated by build.sh on every deployment. Add .gitignore entries per alias (
wwwroot/llm/,wwwroot/llmusage/, ...). WRONG:git add wwwroot/; CORRECT:git add wwwroot/index.ui wwwroot/api/ wwwroot/*.js wwwroot/*.css(hand-written only).
CRITICAL: Selective git add in Module Repos
- NEVER
git add -Aorgit add wwwroot/— module repos contain generated content:build/,*.egg-info/,__pycache__/, CRUD-generated wwwroot subdirs,*.swp/*.swo. Use selective adds:git add mymodule/__init__.py mymodule/init.py;git add wwwroot/index.ui wwwroot/api/ wwwroot/*.js wwwroot/*.css;git add json/ models/.
CRITICAL: Manual Symlink for New Files During Development
- New .ui/.dspy/.js/.css are NOT available until linked:
ln -sf <module>/wwwroot/new_page.ui sage/wwwroot/<module>/,ln -sf <module>/wwwroot/api/new_api.dspy sage/wwwroot/<module>/api/(or re-run build.sh). .js/.css MUST be linked at module wwwroot ROOT level (notscripts//styles/subdirs) — Sage's header.tmpl only scans the root of each linked wwwroot. Symptom: 500 withfpath is None/invalid path— file exists in module but isn't linked.
CRITICAL: build.sh SAGE_ROOT Detection
- Standalone module repos (e.g.
~/repos/mymodule/) are NOT nested under Sage — do NOT assume$SCRIPT_DIR/../..is Sage root. Search candidates:
for candidate in "$SCRIPT_DIR/../.." "$HOME/repos/sage" "$HOME/sage"; do
if [ -d "$candidate/wwwroot" ] && [ -d "$candidate/py3/bin" ]; then
SAGE_ROOT="$(cd "$candidate" && pwd)"; break
fi
done
Sage System Integration — Four Wiring Points
New modules need FOUR changes in the Sage repo before appearing in the UI (missing any one → functions not registered / not installed on fresh deploy / 403 on access / no menu item):
app/sage.py:from mymodule.init import load_mymoduleat top;load_mymodule()insideinit().build.sh: add module to install loop (for m in appbase rbac ... mymodule; do cd $cdir/pkgs; git clone https://git.opencomputing.cn/yumoqing/$m; pip install .; done).load_path.py(RBAC): explicit entries. Roles:any— no login (menu.ui, static JS/CSS, public resources);logined— authenticated (.ui pages, .dspy APIs);owner.superuser/owner.operator— role-restricted admin features. Run:cd ~/repos/sage && ./py3/bin/python load_path.py. load_path.py is the centralized declarative source of truth;set_role_perm.pyis for ad-hoc fixes only and does NOT survive a fresh database restore.global_menu.ui— add toitemsarray insage/wwwroot/global_menu.ui:
,{"name":"modulename","label":"模块名称","icon":"fa fa-icon-name","url":"{{entire_url('/modulename/index.ui')}}","target":"app.sage_main_content"}
CRITICAL: NO conditional menu items — do NOT wrap items in {% if get_user() %} or role checks: Sage's dynamic menu loading does not work, items inside conditionals are never rendered. Place ALL module items unconditionally at items top level. Only exception: dashboard (always visible by design) and explicitly role-hidden items. Access control is RBAC (403 on click is correct behavior), not menu hiding.
CRITICAL: load_path.py Wildcards are FORBIDDEN
- NEVER use
%or*wildcards in any load_path.py — hard policy, not a suggestion. Every path explicit: every .dspy, every CRUD subdir file, every .ui, every image. CRUD subdir standard pattern (5 entries per alias, generated by xls2ui from json/*.json):/{MOD}/{alias},/{MOD}/{alias}/index.ui,/{MOD}/{alias}/get_{alias}.dspy,/{MOD}/{alias}/add_{alias}.dspy,/{MOD}/{alias}/update_{alias}.dspy,/{MOD}/{alias}/delete_{alias}.dspy. - Maintenance: adding .dspy files ⇒ add to
PATHS_LOGINED/PATHS_ANY; removing files ⇒ remove entries. Cross-module audit:find ~/repos -name "load_path.py"+grep -rn '%' --include="load_path.py"— all modules must be wildcard-free.
CRITICAL: Dual-Layer RBAC (per-module script + central fallback)
- Per-module
scripts/load_path.py(callsset_role_perm.pydirectly) can FAIL SILENTLY — observedModuleNotFoundError: No module named 'appPublic.event_dispatcher'for cpcc, product_management, pricing, etc. So ALWAYS ALSO add entries to the central~/repos/sage/load_path.py(different code path, reliable fallback). Symptom if only the per-module script exists and fails: module shows in menu but every click returns 403. Verify:grep -c '/modulename' ~/repos/sage/load_path.py→ > 0.
5. Execute per-module scripts/load_path.py (if it exists)
cd ~/repos/sage && ./py3/bin/python ~/repos/<module>/scripts/load_path.py
RBAC Permission Setup (preferred: per-module)
- Each business module should own
scripts/load_path.py: auto-find Sage root; paths by role tier (any/logined/role-specific); call set_role_perm.py per path; register BOTH directory path (/module/crud_alias— auto-matches index.ui) AND file path (/module/crud_alias/index.ui). Maintenance rule: 每次代码变更如有新 path 出现,需同步更新此脚本. Templates:references/per-module-rbac-pattern.md,references/per-module-load-path-pattern.md. NOTE: some modules (e.g. llmage) use the CENTRAL~/repos/sage/load_path.pyinstead — check before creating a per-module script. - Legacy central approach: add entries to
paths=""in~/repos/sage/load_path.py(/module_name logined,/module_name/index.ui logined,/module_name/menu.ui any,/module_name/api/xxx.dspy logined,/module_name/xxx.js any), run./py3/bin/python load_path.py, restart./stop.sh && ./start.sh. Both set_role_perm.py and load_path.py must use explicit paths — no wildcards in either. - Do NOT reference .css/.js via
<link>/<script>in .ui — ahserver auto-serves them, but they still need RBAC registration in load_path.py.
Multi-Tenant Reseller Module Pattern
Core principle: every table, CRUD definition, and API endpoint scoped by org_id — resellers never share categories/products/configs.
- Table design: every table MUST have
org_id VARCHAR(32) DEFAULT '0'; composite unique indexes(org_id, business_key)e.g.UNIQUE(org_id, product_code); FK lookups include org in JOIN:JOIN t2 ON t1.cat_id = t2.id AND t1.org_id = t2.org_id. - Dynamic attributes:
product_type= STRING identifier (NOT a physical table name) +extra_json(TEXT/LONGTEXT) for reseller-specific attrs (no schema changes); category tables storeproduct_type+product_type_titlefor display (notproduct_table_name); standardized APIs (product_detail,product_use) parse extra_json → returnextra_parsed. - CRUD:
logined_userorgid: "org_id"on ALL list CRUD definitions (auto org-scoped filtering); category dropdown dataurl → API filtering by current user's org;subtables[].urluses../prefix and respects org boundaries. - .dspy: get org via
org_id = (await get_userorgid()) or '0'(or with override:org_id = params_kw.get('org_id', None) or (await get_userorgid()) or '0'); NEVERenv = ServerEnv(); getattr(env,'orgid',...). ALL SQL MUST include org_id in WHERE; create/update/delete MUST verify record ownership; category trees MUST verify parent belongs to same org; dropdown APIs filterWHERE org_id = ${org_id}$. - Operator config:
product_type_configtable with unique(org_id, operator_id, category_id, config_name);config_jsonTEXT → parse toconfig_parsed; fallback queryWHERE operator_id = ${user_id}$ OR operator_id = '0'(0 = global default). - Pitfalls: ① NO
product_table_namephysical routing — breaks across resellers; useproduct_type+extra_json. ② Do NOT preset global category data ininit/data.json— categories are per-reseller; init data only for appcodes/encodings. ③ JOIN without org_id = DATA LEAK (LEFT JOIN pc ON p.category_id = pc.idneedsAND p.org_id = pc.org_id). ④UNIQUE(product_code)without org_id = cross-reseller conflicts. ⑤DBPools()is a@SingletonDecoratorsingleton — neverdb.databases = config.databasesorDBPools(config.databases); justDBPools().sqlorContext(dbname). ⑥ In .py module files get org_id viaenv = ServerEnv(); org_id = getattr(env, 'orgid', None) or getattr(env, 'org_id', '0')— DIFFERENT from .dspy files (await get_userorgid()).
Multi-Module Changes: PR Isolation
When changes span multiple Sage modules, EACH module gets its own feature branch + independent PR (accounting: feat/dataviz-accounting → PR#1, llmage: feat/dataviz-llmage → PR#2, ...). Do NOT combine multi-module changes into one branch — keeps review focused, allows partial merges, prevents blocking unrelated modules.
Cross-Module Changes: Where to Make Changes
- All modules are independent git repos at
~/repos/<module>/— edit/commit/push there. There is NOsage/pkgs/directory (deleted May 2026). For cross-module changes, check each affected repo separately; after each round verify no stale patterns:grep -rn 'old_pattern' ~/repos/<module>/ --include='*.py' --include='*.dspy'. Restart Sage after all code changes (reloads modules from disk).
Database Schema Migration
See references/database-migration-pattern.md: adding tables / changing relationships (1:N → M:N), idempotent migration scripts with safety checks, RBAC for new CRUD endpoints, safe column removal.
Intermediate table removal (e.g. uapiset between upapp and uapi): ① ALTER TABLE child ADD COLUMN parent_id ...; ② move intermediate-table fields (e.g. auth_apiname) to parent; ③ update model JSON (remove old FK, add new FK, update indexes, drop codes references to removed table); ④ update ALL Python code — remove JOINs in every .py of the module repo AND any other referencing modules; ⑤ delete models/{intermediate}.json + json/{intermediate}.json; ⑥ data migration for shared records (pattern: ~/repos/uapi/scripts/migrate_uapi_upappid.py); ⑦ verify grep -rn 'intermediate_table' ~/repos/{mod}/ --include='*.py' returns nothing. Details: references/intermediate-table-removal-pattern.md, references/uapi-uapiset-removal-detail.md.
Development Workflow
CRITICAL: Repos First, Test Second
- Modify source in
~/repos/FIRST (canonical, version-controlled) → 2. git commit immediately → 3. sync to test env (~/test/or deployment dir) → 4. NEVER modify test env directly (changes lost/untracked, vanish on redeploy). Zero tolerance for "I fixed it in test but didn't update repos".
- scp/sed directly to servers is FORBIDDEN (tokentest, production): never scp files or
sed -ion deployment servers. Always: local repos → commit → push → server git pull → pip install. Direct edits bypass version control and get overwritten next deploy. Only exception: one-off read-only diagnostics (grep, mysql queries, log inspection).
Standard development order for a new module: ① create directory structure; ② table definitions in models/; ③ CRUD definitions in json/; ④ backend functions in {module}/init.py — one async function per CRUD op per table, registered via load_{module}() + ServerEnv; ⑤ generate wwwroot/api/*.dspy thin wrappers delegating to init.py functions (bulk generation script in references/bulk-module-creation-pattern.md); ⑥ frontend in wwwroot/ (index.ui + menu.ui first; CRUD UI auto-generated by xls2ui); ⑦ init/data.json if needed; ⑧ pyproject.toml + build.sh; ⑨ README.md with Sage integration steps; ⑩ wire into Sage (4 points above) + run per-module scripts/load_path.py; ⑪ skill/ with SKILL.md + references/assets/scripts.
Parameter Access: .py vs .dspy/.ui
- .dspy/.ui:
params_kwis a global —discountid = params_kw.get('discountid')/{{params_kw.discountid}}. - .py functions called from Jinja2:
request._run_ns.params_kw—env = request._run_ns; val = (request._run_ns.params_kw or {}).get('key', ''). - Wrong attribute names — do NOT use:
request._run_env(doesn't exist; it's_run_ns);request._run_ns.param_kw(singular — it'sparams_kw);request._params_kw(may be empty at Jinja2 render time);request.rel_url.query/request.query(unnecessary — already parsed into_run_ns.params_kw).
sqlExe: No Hardcoded ORDER BY / LIMIT / OFFSET
sqlor handles pagination via sqlPaging() — do NOT hardcode ORDER BY/LIMIT/OFFSET in SQL strings: ns = {"page":1,"sort":"name","order":"desc","rows":20}; recs = await sor.sqlPaging("SELECT * FROM t", ns) → returns {"total": N, "rows": [...]}.
Pitfall: sqlExe returns DictObject — NOT dict-convertible
dict(r) fails: ValueError: dictionary update sequence element #0 has length 1; 2 is required. Access attributes directly: rows = [{"id": r.id, "name": r.name} for r in recs].
dspy Database Access: get_sor_context, NOT DBPools().sqlorContext
In .dspy use async with get_sor_context(request._run_ns, "tenant") as sor: — routes to the correct database per the app's module_dbname config (resolves to pipeline in pipeline-app, sage in Sage). Do NOT hardcode DBPools().sqlorContext("pipeline").
Bricks HBox Column Widths: cwidth vs width%
cwidth in HBox is NOT proportional — it sets fixed pixel width (charsize × cwidth, e.g. cwidth=3 ≈ 42px); differs from Tabular's DataRow where cwidth=10 is the default. For proportional HBox columns use "width": "22%" / "width": "34%" (with HBox "width":"100%"). WRONG: fixed cwidth columns that don't scale. Applies to any hand-built HBox/list row — only Tabular's DataRow handles cwidth proportionally.
Git Initialization & First Push (new module)
- Gitea "Push to create" is DISABLED — create the empty repo on the Gitea web UI (git.opencomputing.cn) BEFORE pushing, else:
Gitea: Push to create is not enabled for users. fatal: Could not read from remote repository. - Auto-generated README → rebase conflict: Gitea's initial README.md vs your local commit →
CONFLICT (add/add). Fix:git checkout --ours README.md && git add README.md && GIT_EDITOR="true" git rebase --continue && git push -u origin main. git rebase --continueopens nano and hangs in non-interactive terminals — always prefixGIT_EDITOR="true".- Sequence: ① write .gitignore FIRST (template:
references/module-gitignore.md); ②git init && git add -A && GIT_EDITOR="true" git commit -m "feat: initial commit message"; ③git remote add origin git@git.opencomputing.cn:yumoqing/{module}.git; ④git pull origin main --rebase(resolve README conflict as above); ⑤git push -u origin main.
CRITICAL: Never Fabricate Pricing or Config Data
Pricing data (元/次, 元/秒, 元/token, etc.) MUST come from official vendor sources only — guessing, estimating, or stale memorized values is a production incident ("生产事故"). If the official pricing page/docs are inaccessible (SPA, login required), tell the user honestly you cannot find it. Do NOT: estimate from similar-tier models, extrapolate from older versions, use memorized pricing that may be stale, or pull from earlier-session skill references (those were guesses too). How to get pricing: ask the user for screenshots/text from the official console/pricing page, or an accessible vendor URL. Applies to: SQL pricing values, YAML price_factors, unit_prices in pricing_program_timing, unit_price in P4 migration scripts — anywhere a wrong number has real billing consequences.
Verification Steps
CRITICAL: Self-Testing Before Commit — MANDATORY
User has zero tolerance for untested code. Before committing: ① deploy to a test environment (venv, deps, app on test port); ② exercise EVERY endpoint (curl/browser each .dspy API and .ui page); ③ watch tail -f logs/*.log during testing for import errors/AttributeErrors/500s; ④ cross-validate against working Sage code (~/repos/sage/wwwroot/ real .dspy files). If you cannot set up a test environment, say so explicitly and do NOT commit. Pattern: write → self-deploy → self-test → fix → commit only when verified.
CRITICAL: dspy Batch Audit Before Commit — MANDATORY PRE-COMMIT GATE
Run before EVERY commit touching .dspy; fix violations FIRST, then commit (do not commit-and-fix-later). Pre-loaded globals in dspy context (NO import needed): json, datetime, time, os, debug/exception/error/info/warning/critical (appPublic.log), DBPools, get_sor_context, DictObject, get_user, params_kw, format_exc, password_encode, password_decode, curDateString, timestampstr, getID, functools.partial, ahserver.filestorage.FileStorage, RegisterFunction, DBFilter, ArgsConvert (sqlor.filter), plus ALL ServerEnv-registered functions from every module's load_*().
- Module-internal imports MUST go through
load_*():from mymodule.utils import fooin dspy is NOT allowed — export viaenv.foo = fooinload_mymodule(). - Audit command (output MUST be empty):
grep -rn "^import\|^from" wwwroot/ --include='*.dspy' | grep -v "sqlor.filter"— remaining imports must be removed (pre-loaded) or moved to load_*() export. - Workflow: write/edit → audit → fix → re-audit (zero violations) → only then git add → commit. See
references/dspy-batch-fix-pattern.md. - 500/NoneType with a stack trace: read the file named in the error FIRST — do not chase tangential issues. Real case: 3×
currency_stats.dspy NoneType"fixed" in init.py try/except, etl.py, SQL columns — all irrelevant; root cause was a JSON-format DSPY where a Python-script was expected.
CRITICAL: Database Name Consistency Check
模块取库名不硬编码,统一从宿主应用获取。验证点:① 模块内(.py)用 ServerEnv().get_module_dbname('模块名'),.dspy 里直接 get_module_dbname('模块名')(全局,无需 import)——不要在模块里写 DBNAME = "xxx";② 宿主应用 app/{appname}.py 定义 get_module_dbname(m) 返回模块 m 的真实库名,并在 init() 里挂到 ServerEnv(env.get_module_dbname = get_module_dbname,见 web-application-spec);③ 应用入口的 get_module_dbname() 返回值必须匹配实际库名。Pitfalls: 模块硬编码 DBNAME = "entcms" 而实际库是 "ocai_cms" → 所有查询 table-not-found;应用 get_module_dbname() 返回 'sage' → dspy 查错库。
Post-Deployment Testing (equally mandatory)
Login for a session cookie, then curl every affected endpoint expecting 200 (not 403/500); never say "可以测试了" without running these yourself:
curl -s -c /tmp/cookies.txt -X POST 'http://localhost:9180/rbac/user/login.ui' \
-H 'Content-Type: application/x-www-form-urlencoded' -d 'username=test&password=test123'
for path in /module/page /module/api/endpoint.dspy /imgs/icon.svg; do
code=$(curl -s -o /dev/null -w '%{http_code}' -b /tmp/cookies.txt "http://localhost:9180$path")
echo "$code $path" # must be 200
done
Pre-commit pattern checks: async with db.sqlorContext(dbname) as sor: (NOT sor = DBPools().sqlorContext(dbname)); no imports in dspy except from sqlor.filter import DBFilter; return not print(json.dumps(...)); getID() not uuid(); await get_user() not get_user(); Python module DBNAME constants match config.json; get_module_dbname() return value matches the app's actual database.
Final Deployment Checklist
- Module loads via load_{modulename}(); imported in
app/sage.py; in build.sh install loop - RBAC permissions in load_path.py and loaded via
python load_path.py; menu item in global_menu.ui (NOT inside {% if %} conditionals) - wwwroot symlinked to
sage/wwwroot/{modulename}/; all exposed functions work in frontend scripts - DB ops follow sqlor; JSON configs have
editablewith top-level new/update/delete_data_url; same-moduleentire_url()refs use../ - Init data loads; pyproject.toml builds; skill/SKILL.md complete with references/assets/scripts organized
References / Scripts Index
references/bricks-ui-pitfalls.md— id placement (widget level), Button click, Popup/Form, script actiontype Jinja2 limitsreferences/dspy-execution-and-module-structure.md— dspy execution model, pre-loaded globals, Tree widget data format, module symlink patternsreferences/json-config-pattern.md— full CRUD JSON config templatereferences/crud-json-toolbar-bind-pattern.md— custom toolbar buttons + PopupWindow bind patternreferences/web-app-and-dspy-spec.md— debug output best practices (debug_params), injecting new functions into .dspy execution env via self.y_envreferences/read-only-module-pattern.md— read-only/dashboard module templatereferences/custom-create-dspy-required-fields.md— custom create dspy with auto-generated business codesreferences/model-change-rebuild-pitfalls.md— .xlsx/.json load order, get_code.dspy module.table resolution failure, collation mismatchreferences/bulk-module-creation-pattern.md— ordered creation sequence, bulk .dspy generation script, init.py templates, cascade delete, subagent validation checklistreferences/independent-web-app-pattern.md— standalone ahserver web apps (own venv/port/config.json)references/rbac-init-pattern.md— bootstrapping org types/roles/users/role-permissions for independent appsreferences/test-env-module-deployment.md— deploying modules from ~/test into Sage test env (pip install, symlink, RBAC, restart, git push)references/public-endpoint-cron-pattern.md— dspy endpoints without auth for cron/background jobsreferences/hub-dashboard-pattern.md— hub entry page (stat cards + nav buttons + urlwidget content area)references/registry-table-pattern.md— registry/mapping table + dynamic table routingreferences/llmage-openai-endpoint-pattern.md— OpenAI-compatible endpoints on llmage (/v1/chat/completions, /v1/video/generations, /v1/image/generations)references/llmage-data-model.md— llmage key tables, JOIN patterns, apiname in llm_api_map NOT llm, subquery alias scopingreferences/api-integration-pattern.md— uapi/llmage 4-table API integration (no code changes)references/tencent-hunyuan-api.md— Tencent Hunyuan model specs, pricing, Sage integrationreferences/external-api-integration-module-pattern.md— third-party API integration modulesharnessed-module-developmentskill →references/vendor-callback-pattern.md— vendor webhook/callback endpointsreferences/rbac-tables.md— RBAC table chain (organization→role→userrole→rolepermission→permission) + init orderreferences/volcengine-ark-api.md— Ark real-person portrait asset API: AK/SK signing, asset group/workflow, hybrid module (own client + uapi)references/downapp-proxy-api-pattern.md— vendor proxy API: centralized vendor config table, org→resource mapping, ownership validation, multi-vendor factoryreferences/configurable-rule-engine-pattern.md— registry-based dynamic rule engine (discounts/permissions/workflows)references/platform-cross-org-table-pattern.md— cross-org (platform-level) tables without resellerid isolation, unified supplier mapping, menu grouping with separatorsreferences/cache-control-pattern.md— config.jsonmodule_cachetoggle for per-module in-memory cachingreferences/sage-deployment-pitfalls.md— site-packages sync, DSPY async limitations, CRUD before_insert hooks, browser testing requirementreferences/pipeline-app-deployment-pitfalls.md— umbrella repo recovery, bricks dist build, dspy return-outside-async-with, RBAC static file permissionsreferences/feature-enablement-three-layer-pattern.md— unlocking features gated behind hardcoded UI/dspy/backend limitsreferences/cross-database-query-pattern.md— cross-db JOIN with fully-qualified names (e.g.product_management.productfromdiscountdb)references/social-platform-pattern.md— content/social module: counters (R+U), toggle likes, nested comments (parent_id+reply_count), view counting, paid downloads, media classification, paginated feedsreferences/ai-compute-service-pattern.md— GPU services (4090 → ahserver HTTP API + nginx IP whitelist → llmage+uapi → Hermes skill)references/dspy-error-handling.md— IntegrityError handling, UPSERT, formidmanagement in .dspyreferences/dspy-nonetype-defense.md— mandatory try/except wrappers on all DSPY-called functionsreferences/dspy-pitfalls.md— return format, sqlor API signatures, param cleaning, return-inside-async-with, dbname naming, $placeholder escapingreferences/module-extraction-pattern.md— extracting embedded modules from umbrella repos viagit subtree splitreferences/bricks-dist-build.md— mandatory bricksdist/build + symlink after fresh clone/restorereferences/crud-list-query-optimization-pattern.md— exclude TEXT/LONGTEXT from SELECT, composite indexes on (filter_field, sort_field)references/sage-config-and-db-export.md— non-standard config.json (bare objects), RC4 password decryption, table discovery from models/*.json, mysqldump exportreferences/i18n-merge-pattern.md— consolidating translations intowwwroot/i18n/viascripts/merge_i18n.py(uses build.sh module list, ~/repos paths)references/popup-form-refresh-pattern.md— refresh parent content after Popup Form submit (target: app.sage_main_content)references/bricks-tabular-code-field-pattern.md— Tabular code fields: staticdatavs dynamicdataurl+valueField/textField, undefined.length errorsreferences/feature-page-addition-checklist.md— 7-step new feature page workflow (dspy → ui → menu → RBAC → CSS → commit per repo → deploy)references/supplychain-attachment-pattern.md— filemgr attachments + drag-drop Form upload (Form submit_url + UiFile multiple)references/marketing-discount-setting.md— product discount override with base-comparison validation and flag patternreferences/toolbar-ensure-pattern.md— one-click toolbar check-or-create (actiontype: "dspy"+ params_mapping) to ensure a related record existsreferences/subtable-fk-auto-populate.md— auto-populating FKs in subtable add forms (editexclouded + new_data_url query params)references/database-migration-pattern.md— full schema migration workflowreferences/intermediate-table-removal-pattern.md— junction table removal with SQL examplesreferences/uapi-uapiset-removal-detail.md— concrete session example (uapiset removal from uapi module)references/per-module-rbac-pattern.md— per-module load_path.py script templatereferences/per-module-load-path-pattern.md— per-module template with role-tier separationreferences/module-gitignore.md— .gitignore template for module reposreferences/dspy-batch-fix-pattern.md— systematic dspy audit-fix approachreferences/umbrella-repo-module-extraction.md— extracting embedded modules with preserved git historyreferences/app-level-wwwroot-pattern.md— unified app-level wwwroot (frontend files centralized; modules keep Python code, move static files)scripts/dspy_audit.sh— pre-commit dspy audit (run before any commit touching .dspy)scripts/module_compliance_audit.py— batch compliance audit (init/data.json format, load_path.py existence, model codes parentid compliance) across module repos