58 KiB
Raw Blame History

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。
开发业务模块Python 包)时加载我,否则会产出带 app.py 的错误模块结构
Task involves implementing a component with bricks-framework frontend and sqlor database backend
Development follows the documented directory structure with wwwroot, models, json, and init directories
Task involves creating read-only/dashboard modules that display data without CRUD (no models/json needed)

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.py entry point, NO own port, NO own Dockerfile/service, NO own deploy script. It only runs because a host application calls load_{module}(). A module that carries its own app.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 fail NameError: 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__.py import line; ③ init.py env.xxx = xxx in load_{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) calling create_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 500 function 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/query DO NOT EXIST — only sor.C/U/D/R/I/sqlExe). Mandatory validation: move misplaced files to package dir; check model JSONs have summary (array primary); check CRUD JSONs have tblname + params.editable; grep for fake sqlor APIs → fix to sor.C/U/D; dspy audit (no imports/print/uuid); py_compile all .py. See references/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 forward output_dir if present; do NOT build {'task_type': 'separate', ...}. Symptom: client sends separate_full but worker log shows separate; 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/await are 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 in async def myfunc(request, **ns): and awaits it; a bare result expression returns None. See references/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 in app/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: permission permcode/permnamepath/name; users created_datecreated_at; organization org_nameorgname.
  • Pitfall: {{id}} in uapi response templates resolves to Python built-in id() — 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 an id field.
  • Pitfall: uapiio input_fields MUST be a JSON array — bricks.js LlmIO calls this.input_fields.forEach(...); an object {"field":{...}} throws TypeError: this.input_fields.forEach is not a function. Use array form: [{"name":"prompt","label":"用户输入","uitype":"text","required":true}]. Canonical reference: existing ktv_asr_transcribe_io record.
  • Pitfall: json.dumps() in uapi data templates → data=None{{json.dumps(prompt, ensure_ascii=False)}} can render silently to None (logs body=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: return inside async with → silent NoneType — returning inside async with db.sqlorContext(...) as sor: returns None → return data type error, <class 'NoneType'>. Collect results inside the block, return AFTER it exits. Minimal repro: return {'ok': True} inside async with also fails.
  • Pitfall: sor.C() silently drops records when created_at missing — sqlor does NOT auto-set timestamps; with created_at TIMESTAMP NOT NULL the insert silently vanishes. Always set ns['created_at'] = curDateString() (and ns['org_id'] = (await get_userorgid()) or '0'). Symptom: dspy returns {"success": true} but SELECT returns zero rows.
  • Pitfall: Bash heredoc \$ escapes corrupt sqlor placeholderscat > 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 -A must 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. See references/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) use any role.
  • 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 with id: "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 to subtables[].url, browserfields dataurl, 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_url passes a field as query param (?supplier_id=...) that is also in editexclouded, form POST body + URL params merge into params_kw as a LIST → sor.C gets a list → MySQL Operand should contain 1 column(s). Fix: remove the query param from new_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 editable segment, else 新增/编辑/删除 buttons can't submit. It defines three target URLs pointing at wwwroot/api/ .dspy scripts.
  • new_data_url/update_data_url/delete_data_url MUST be at params TOP LEVEL — NOT nested inside editable. xls2ddl reads them from params top level ({% if new_data_url %}); nesting silently generates the default add_{tablename}.dspy, bypassing any custom create logic. Real case (2026-07-13): discount module nested them → created_by never set by custom handler; fixed by moving to params top level with /module/api/ absolute paths.
  • wwwroot is the web root and invisible in URLs — paths must NOT contain wwwroot/; 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 in wwwroot/api/. Missing editablebuild_add_form()/build_update_form() fail on undefined this.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. dspy and function are 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 inside script actiontype — 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_list instead 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 via os.environ/os.read()/sys.stdin.
  • Return consistent JSON: status, data, optional message/total. Validate inputs; call business logic from init.py-registered functions.
  • Logging: use debug_params('name', dict_obj) instead of debug(f'{dict_obj=}') (dumps whole objects). To add a new global available in ALL .dspy, inject via self.y_env in ahserver/processorResource.py (see references/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 like create_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 use ServerEnv() for per-request user/session context. get_user()/get_userid()/get_userorgid() are registered on request._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 in load_{module}() (startup) and for startup-time globals with no request. Detection: grep -n 'env = ServerEnv()' module/init.py and check the next 6 lines for get_user/get_userorgid/get_userid. Real case (2026-07-13): discount create_marketing → created_by always NULL, 7 functions affected (sibling generate_promo_code correctly used request._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 symlink sage/wwwroot/tenant -> ../pkgs/tenant/wwwroot; NOT under /d/apitest/pipeline-app/pkgs/tenant/. Each application installs modules into its OWN pkgs/ — 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}.json in models/; CRUD definitions as {tablename_or_alias}.json in json/.
  • Bulk xlsx→json conversion: scripts/xlsx2json_models.py converts all .xlsx model files across Sage modules to JSON in one pass.
  • CRITICAL: 模块取库名禁止硬编码 DBNAME —— 库名由宿主应用决定,不是模块自己写死。模块取库名统一用 get_module_dbname('模块名').dspy 里直接调用,是 ServerEnv 注入的全局,无需 importServerEnv().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). parentid in appcodes_kv must match an appcodes.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}; both id and parentid are VARCHAR(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 in models/*.json codes cond, update BOTH init/data.json and all referencing model files.

  • Pitfall: YAML hierarchy_flg0 fullwidth 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 → future products), 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 name MUST 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.

  • 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 except api|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: edit json/{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 with json.dump().
  • Pitfall: xls2ddl data_new_tmpl {{summary[0].pkey}}WHERE None = %s — model JSONs use primary (an array), NOT pkey; 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]}} in xls2ddl/xls2ddl/tmpls.py line 308.
  • When json/ config cannot express needed logic: ① create custom dspy in wwwroot/api/ (e.g. api/add_user.dspy); ② point new_data_url/update_data_url/delete_data_url at it; ③ register paths in scripts/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_url query params AND editexclouded → 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 like contract_code (see references/custom-create-dspy-required-fields.md); clean _text suffix fields before sor.U/C (Tabular sends these from code lookups); preserve password on update (pop empty password from ns); try/except with format_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 to sage/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 (uitype code + dataurl) AND editexclouded is 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') at bricks.UiCode.build_options on 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 with git pull only, skipping build.sh. Long-term: fix xls2ui. Verification (per alias): compare source json/{alias}.json params.editexclouded vs generated wwwroot/{alias}/index.ui row_options.editexclouded.
  • Pitfall: cheight on CRUD index.ui makes title row fill viewport — generated files contain "cheight":40 on both outer VBox and Tabular options; remove ALL cheight properties (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_fields redaction, DBFilter parsing). Do NOT hand-write replacements — e.g. creating api/llm_list.dspy to replace generated get_llm.dspy just for _text display; instead fix the dataurl API 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.json exists, the framework auto-generates wwwroot/{alias}/get_{table}_list.dspy. A hand-written wwwroot/api/get_{table}_list.dspy SHADOWS it → 500s (filters like org_id on missing fields), 403s (bypasses framework RBAC/logined_userorgid), silent data leakage (misses confidential_fields redaction). Rule: if json/{alias}_list.json exists, do NOT create api/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 -A or git 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/.
  • 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 (not scripts//styles/ subdirs) — Sage's header.tmpl only scans the root of each linked wwwroot. Symptom: 500 with fpath 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):

  1. app/sage.py: from mymodule.init import load_mymodule at top; load_mymodule() inside init().
  2. 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).
  3. 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.py is for ad-hoc fixes only and does NOT survive a fresh database restore.
  4. global_menu.ui — add to items array in sage/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 (calls set_role_perm.py directly) can FAIL SILENTLY — observed ModuleNotFoundError: 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.py instead — 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 store product_type + product_type_title for display (not product_table_name); standardized APIs (product_detail, product_use) parse extra_json → return extra_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[].url uses ../ 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'); NEVER env = 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 filter WHERE org_id = ${org_id}$.
  • Operator config: product_type_config table with unique (org_id, operator_id, category_id, config_name); config_json TEXT → parse to config_parsed; fallback query WHERE operator_id = ${user_id}$ OR operator_id = '0' (0 = global default).
  • Pitfalls: ① NO product_table_name physical routing — breaks across resellers; use product_type + extra_json. ② Do NOT preset global category data in init/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.id needs AND p.org_id = pc.org_id). ④ UNIQUE(product_code) without org_id = cross-reseller conflicts. ⑤ DBPools() is a @SingletonDecorator singleton — never db.databases = config.databases or DBPools(config.databases); just DBPools().sqlorContext(dbname). ⑥ In .py module files get org_id via env = 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 NO sage/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

  1. 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 -i on 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_kw is a global — discountid = params_kw.get('discountid') / {{params_kw.discountid}}.
  • .py functions called from Jinja2: request._run_ns.params_kwenv = 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's params_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 --continue opens nano and hangs in non-interactive terminals — always prefix GIT_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 foo in dspy is NOT allowed — export via env.foo = foo in load_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

模块取库名不硬编码,统一从宿主应用获取。验证点:① 模块内(.pyServerEnv().get_module_dbname('模块名').dspy 里直接 get_module_dbname('模块名')(全局,无需 import——不要在模块里写 DBNAME = "xxx";② 宿主应用 app/{appname}.py 定义 get_module_dbname(m) 返回模块 m 的真实库名,并在 init() 里挂到 ServerEnvenv.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 editable with top-level new/update/delete_data_url; same-module entire_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 limits
  • references/dspy-execution-and-module-structure.md — dspy execution model, pre-loaded globals, Tree widget data format, module symlink patterns
  • references/json-config-pattern.md — full CRUD JSON config template
  • references/crud-json-toolbar-bind-pattern.md — custom toolbar buttons + PopupWindow bind pattern
  • references/web-app-and-dspy-spec.md — debug output best practices (debug_params), injecting new functions into .dspy execution env via self.y_env
  • references/read-only-module-pattern.md — read-only/dashboard module template
  • references/custom-create-dspy-required-fields.md — custom create dspy with auto-generated business codes
  • references/model-change-rebuild-pitfalls.md — .xlsx/.json load order, get_code.dspy module.table resolution failure, collation mismatch
  • references/bulk-module-creation-pattern.md — ordered creation sequence, bulk .dspy generation script, init.py templates, cascade delete, subagent validation checklist
  • references/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 apps
  • references/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 jobs
  • references/hub-dashboard-pattern.md — hub entry page (stat cards + nav buttons + urlwidget content area)
  • references/registry-table-pattern.md — registry/mapping table + dynamic table routing
  • references/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 scoping
  • references/api-integration-pattern.md — uapi/llmage 4-table API integration (no code changes)
  • references/tencent-hunyuan-api.md — Tencent Hunyuan model specs, pricing, Sage integration
  • references/external-api-integration-module-pattern.md — third-party API integration modules
  • harnessed-module-development skill → references/vendor-callback-pattern.md — vendor webhook/callback endpoints
  • references/rbac-tables.md — RBAC table chain (organization→role→userrole→rolepermission→permission) + init order
  • references/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 factory
  • references/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 separators
  • references/cache-control-pattern.md — config.json module_cache toggle for per-module in-memory caching
  • references/sage-deployment-pitfalls.md — site-packages sync, DSPY async limitations, CRUD before_insert hooks, browser testing requirement
  • references/pipeline-app-deployment-pitfalls.md — umbrella repo recovery, bricks dist build, dspy return-outside-async-with, RBAC static file permissions
  • references/feature-enablement-three-layer-pattern.md — unlocking features gated behind hardcoded UI/dspy/backend limits
  • references/cross-database-query-pattern.md — cross-db JOIN with fully-qualified names (e.g. product_management.product from discount db)
  • 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 feeds
  • references/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, form id management in .dspy
  • references/dspy-nonetype-defense.md — mandatory try/except wrappers on all DSPY-called functions
  • references/dspy-pitfalls.md — return format, sqlor API signatures, param cleaning, return-inside-async-with, dbname naming, $placeholder escaping
  • references/module-extraction-pattern.md — extracting embedded modules from umbrella repos via git subtree split
  • references/bricks-dist-build.md — mandatory bricks dist/ build + symlink after fresh clone/restore
  • references/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 export
  • references/i18n-merge-pattern.md — consolidating translations into wwwroot/i18n/ via scripts/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: static data vs dynamic dataurl+valueField/textField, undefined.length errors
  • references/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 pattern
  • references/toolbar-ensure-pattern.md — one-click toolbar check-or-create (actiontype: "dspy" + params_mapping) to ensure a related record exists
  • references/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 workflow
  • references/intermediate-table-removal-pattern.md — junction table removal with SQL examples
  • references/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 template
  • references/per-module-load-path-pattern.md — per-module template with role-tier separation
  • references/module-gitignore.md — .gitignore template for module repos
  • references/dspy-batch-fix-pattern.md — systematic dspy audit-fix approach
  • references/umbrella-repo-module-extraction.md — extracting embedded modules with preserved git history
  • references/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