Feat v0.8.5 extension composability (#72)
All checks were successful
CI/CD / detect-changes (push) Successful in 4s
CI/CD / test-runners (push) Has been skipped
CI/CD / e2e-smoke (push) Has been skipped
CI/CD / test-frontend (push) Successful in 6s
CI/CD / test-go-pg (push) Successful in 2m44s
CI/CD / test-sqlite (push) Successful in 2m54s
CI/CD / build-and-deploy (push) Successful in 52s
All checks were successful
CI/CD / detect-changes (push) Successful in 4s
CI/CD / test-runners (push) Has been skipped
CI/CD / e2e-smoke (push) Has been skipped
CI/CD / test-frontend (push) Successful in 6s
CI/CD / test-go-pg (push) Successful in 2m44s
CI/CD / test-sqlite (push) Successful in 2m54s
CI/CD / build-and-deploy (push) Successful in 52s
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com> Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit was merged in pull request #72.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# DESIGN — Extension Composability
|
||||
|
||||
**Version:** v0.8.4
|
||||
**Status:** Draft
|
||||
**Version:** v0.8.5
|
||||
**Status:** Implemented
|
||||
**Author:** Jeff / Claude session 2026-04-02
|
||||
|
||||
---
|
||||
|
||||
126
docs/DESIGN-workflow-redesign.md
Normal file
126
docs/DESIGN-workflow-redesign.md
Normal file
@@ -0,0 +1,126 @@
|
||||
# DESIGN — Workflow System Redesign (0.9.x)
|
||||
|
||||
**Version:** v0.9.0
|
||||
**Status:** Draft
|
||||
**Author:** Jeff / Claude session 2026-04-03
|
||||
|
||||
---
|
||||
|
||||
## Problem
|
||||
|
||||
The workflow system spans ~7,600 lines across 25+ files and was built
|
||||
incrementally from v0.3.x through v0.7.10. It works, but several
|
||||
concepts are redundant, primitives are buried, and the Starlark module
|
||||
is read-only. Before shipping reference extensions (v0.10.x) that
|
||||
build on workflows, the system needs cleanup and promotion of reusable
|
||||
primitives.
|
||||
|
||||
## Audit Summary
|
||||
|
||||
Full audit in `/config/Downloads/AUDIT-workflow-0.9.md`. Classification:
|
||||
|
||||
### KEEP — Core primitives that survive
|
||||
|
||||
| Component | File(s) | Lines | Rationale |
|
||||
|-----------|---------|-------|-----------|
|
||||
| Engine core | `workflow/engine.go` | ~400 | Stateless state machine — Start, Advance, Cancel, version-pinned execution |
|
||||
| Conditional routing | `workflow/routing.go` | ~500 | Pure functions, 8 operators, well-tested (497 lines of tests) |
|
||||
| Automated processing | `workflow/automated.go` | ~300 | Starlark hook execution, cycle guard (max 10) |
|
||||
| Scanner | `workflow/scanner.go` | ~200 | Background SLA + staleness checks, 5-minute interval |
|
||||
| Signoff system | engine.go + handlers | ~400 | Multi-party approve/reject with quorum, role-gated |
|
||||
| Assignment queue | handlers | ~300 | Claim/unclaim/complete/cancel lifecycle |
|
||||
| Version snapshots | store + models | ~200 | Immutable snapshots, version-pinned instances |
|
||||
| Typed form system | `models/workflow.go` | ~300 | 8 field types, fieldsets, conditional visibility |
|
||||
| Store interface | `store/workflow_iface.go` | 61 methods | Complete lifecycle coverage |
|
||||
|
||||
### OBE — Superseded by new design
|
||||
|
||||
| Concept | Superseded by |
|
||||
|---------|---------------|
|
||||
| Per-stage `audience` field | Multi-page packages with mixed access declarations |
|
||||
| `entry_mode` (public_link/team_only) | Package-level `scope: adoptable` + per-page access |
|
||||
| `stage_mode` proliferation (4 values) | Collapse to 3: form / delegated / automated |
|
||||
| `stage_type` (simple/dynamic/automated) | Redundant with `starlark_hook` presence check |
|
||||
| `AdoptTeamWorkflow` clone | Package adoption model |
|
||||
| `ExportWorkflowPackage` endpoint | Standard package export |
|
||||
|
||||
### PROMOTE — Buried features to expose as kernel primitives
|
||||
|
||||
1. **Roles → kernel primitive**: `team_user_roles` table, manifest `requires_roles`,
|
||||
team admin UI, kernel middleware, Starlark SDK
|
||||
2. **Typed forms → SDK primitive**: Move from workflow models to `forms` package,
|
||||
FE SDK `sw.forms.render()` / `sw.forms.validate()`
|
||||
3. **Conditional routing → SDK primitive**: Starlark `routing.evaluate(rules, data)`
|
||||
4. **Workflow Starlark module → full read/write**: `workflow.start()`, `.advance()`,
|
||||
`.cancel()`, `.submit_signoff()`
|
||||
|
||||
---
|
||||
|
||||
## Technical Debt
|
||||
|
||||
### Starlark Converter Duplication
|
||||
|
||||
Three files contain nearly identical Go↔Starlark conversion functions:
|
||||
|
||||
| File | Functions |
|
||||
|------|-----------|
|
||||
| `workflow/automated.go` | `goToStarlark`, `starlarkDictToMap`, `starlarkToGo` |
|
||||
| `handlers/workflow_hooks.go` | `starlarkDictToMap`, `starlarkToGo`, `jsonToStarlark` |
|
||||
| `sandbox/workflow_module.go` | `goValToStarlark` |
|
||||
|
||||
**Fix:** Consolidate into `sandbox/convert.go`.
|
||||
|
||||
### Snapshot Format Inconsistency
|
||||
|
||||
Two formats exist (wrapped `{stages, workflow}` vs legacy flat array).
|
||||
Three copies of the parser across engine, instance handlers, and
|
||||
assignment handlers.
|
||||
|
||||
**Fix:** Consolidate into one exported function. Standardize on wrapped format.
|
||||
|
||||
---
|
||||
|
||||
## Sequencing
|
||||
|
||||
| Version | Feature | Dependencies |
|
||||
|---------|---------|-------------|
|
||||
| **v0.9.0** | Starlark converter consolidation + snapshot format cleanup | None |
|
||||
| **v0.9.1** | `team_user_roles` table + management API + admin UI | None |
|
||||
| **v0.9.2** | `scope: adoptable` manifest field + adoption flow with role auto-populate | v0.9.1 |
|
||||
| **v0.9.3** | Promote typed forms to SDK primitive (`sw.forms`) | None |
|
||||
| **v0.9.4** | Deprecate `stage_type`, collapse `stage_mode` to 3 values | None |
|
||||
| **v0.9.5** | Full read/write workflow Starlark module | v0.9.0 |
|
||||
| **v0.9.6** | Conditional routing as SDK primitive | v0.9.5 |
|
||||
| **v0.9.7** | Multi-surface manifest + kernel route resolution | None |
|
||||
| **v0.9.8** | Wire roles into surface access (`access: role:X`) + kernel middleware | v0.9.1, v0.9.7 |
|
||||
|
||||
Each step is CI-green independently. The first four are the high-value items.
|
||||
|
||||
---
|
||||
|
||||
## Files Affected (by component)
|
||||
|
||||
### Converter Consolidation (v0.9.0)
|
||||
- New: `sandbox/convert.go`
|
||||
- Modified: `workflow/automated.go`, `handlers/workflow_hooks.go`, `sandbox/workflow_module.go`
|
||||
|
||||
### Team Roles (v0.9.1)
|
||||
- New: migration (PG + SQLite), `store/team_roles.go`, `handlers/team_roles.go`
|
||||
- Modified: manifest validation, adoption flow, team admin UI
|
||||
|
||||
### Typed Forms (v0.9.3)
|
||||
- New: `forms/` package (extracted from `models/workflow.go`)
|
||||
- New: `sw.forms` SDK module
|
||||
- Modified: workflow engine to import from `forms/` instead of inline
|
||||
|
||||
### Workflow Starlark Module (v0.9.5)
|
||||
- Modified: `sandbox/workflow_module.go` — add start/advance/cancel/signoff
|
||||
- Modified: `workflow/engine.go` — extract interface to break circular import
|
||||
|
||||
---
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Rewriting the workflow engine. The state machine is correct and stays.
|
||||
- Adding a visual workflow builder. That's an extension concern, not kernel.
|
||||
- Multi-tenancy changes. Team scoping works as designed.
|
||||
@@ -77,7 +77,10 @@ Every package has a `manifest.json` at its root. Example for a surface:
|
||||
| `api_schema` | no | OpenAPI documentation for extension API routes (see below) |
|
||||
| `db_tables` | no | Table definitions (see below) |
|
||||
| `settings` | no | User-configurable settings schema |
|
||||
| `exports` | libraries | Functions exported for other packages |
|
||||
| `exports` | no | Functions exported for cross-package calls via `lib.require()` |
|
||||
| `depends` | no | Array of package IDs this package depends on |
|
||||
| `slots` | no | Named UI injection points for host surfaces |
|
||||
| `contributes` | no | Slot contributions into other surfaces |
|
||||
| `hooks` | no | Event bus subscriptions |
|
||||
| `config_section` | no | Settings/Admin panel injection (see below) |
|
||||
| `capabilities` | no | Environment requirements (see below) |
|
||||
@@ -194,6 +197,117 @@ sandbox permission model.
|
||||
In Starlark, check permissions inline via `req["permissions"]` or call
|
||||
`permissions.check(user_id, "image-gen.use")`.
|
||||
|
||||
## Extension Composability
|
||||
|
||||
Extensions compose with each other through three mechanisms: manifest-declared
|
||||
**slots** (UI injection points), **contributions** (UI components injected into
|
||||
those slots), and cross-package **function calls** via `lib.require()`.
|
||||
|
||||
### Slots — Host Surfaces Declare Injection Points
|
||||
|
||||
A surface declares named slots in its manifest where other extensions can
|
||||
inject UI components:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "notes",
|
||||
"type": "surface",
|
||||
"slots": {
|
||||
"toolbar-actions": {
|
||||
"description": "Toolbar action buttons",
|
||||
"context": {
|
||||
"noteId": "string",
|
||||
"getContent": "function — returns note body",
|
||||
"setContent": "function — replaces note body"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The surface renders slot contents using the SDK helper:
|
||||
|
||||
```javascript
|
||||
html`<div class="toolbar">
|
||||
${sw.slots.renderAll('notes:toolbar-actions', {
|
||||
noteId: note.id,
|
||||
getContent: () => editor.getValue(),
|
||||
setContent: (text) => editor.setValue(text),
|
||||
})}
|
||||
</div>`
|
||||
```
|
||||
|
||||
### Contributions — Extensions Inject UI
|
||||
|
||||
An extension declares which slots it contributes to:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "note-dictate",
|
||||
"type": "extension",
|
||||
"contributes": {
|
||||
"notes:toolbar-actions": {
|
||||
"label": "Dictate",
|
||||
"icon": "🎤",
|
||||
"description": "Voice-to-text dictation"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In its JavaScript, it registers the component:
|
||||
|
||||
```javascript
|
||||
sw.slots.register('notes:toolbar-actions', {
|
||||
id: 'note-dictate',
|
||||
priority: 200,
|
||||
component: ({ noteId, setContent, getContent }) => {
|
||||
// ... component implementation
|
||||
return html`<button class="sw-btn sw-btn--ghost">🎤</button>`;
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Contributions are soft-coupled — install order doesn't matter. The admin
|
||||
can view all slots and contributors at **Admin > Packages** or via
|
||||
`GET /api/v1/admin/slots`.
|
||||
|
||||
### Cross-Package Function Calls
|
||||
|
||||
Any package that declares `exports` in its manifest can be called by other
|
||||
packages via `lib.require()`. The caller declares the dependency:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "note-ai",
|
||||
"depends": ["llm-bridge"],
|
||||
"permissions": ["api.http"]
|
||||
}
|
||||
```
|
||||
|
||||
In Starlark:
|
||||
|
||||
```python
|
||||
llm = lib.require("llm-bridge")
|
||||
result = llm.complete([{"role": "user", "content": prompt}])
|
||||
```
|
||||
|
||||
The called function runs with the *target* package's permissions, not the
|
||||
caller's. This is the same security model as library packages.
|
||||
|
||||
### Slot Naming Convention
|
||||
|
||||
Slot names follow `{host-package-id}:{slot-name}`. The colon separates the
|
||||
namespace from the slot. Standard slots for first-party packages:
|
||||
|
||||
| Slot | Host | Use Case |
|
||||
|------|------|----------|
|
||||
| `notes:toolbar-actions` | notes | Dictation, AI tools, formatting |
|
||||
| `notes:note-footer` | notes | Related items, AI summary |
|
||||
| `chat:composer-tools` | chat | Image gen, file attach |
|
||||
| `chat:message-actions` | chat | Reactions, translate, bookmark |
|
||||
| `chat:image-actions` | chat | Regen, edit, upscale |
|
||||
|
||||
## Starlark Sandbox API
|
||||
|
||||
Starlark scripts run server-side with a 1M operation budget and no
|
||||
|
||||
@@ -68,10 +68,66 @@ Only `manifest.json` is required. All other directories are optional and include
|
||||
| `db_tables` | Table definitions with columns and indexes |
|
||||
| `settings` | User-configurable settings with type, label, description, default |
|
||||
| `hooks` | Event bus subscription patterns |
|
||||
| `exports` | Functions exported by library packages |
|
||||
| `exports` | Functions exported for cross-package calls via `lib.require()` |
|
||||
| `depends` | Array of package IDs this package depends on |
|
||||
| `slots` | Named UI injection points (host surfaces declare these) |
|
||||
| `contributes` | Slot contributions this package injects into other surfaces |
|
||||
| `capabilities` | Environment requirements: `{"required": [...], "optional": [...]}` |
|
||||
| `schema_version` | Integer for additive schema migrations |
|
||||
|
||||
## Composability: Slots and Contributions
|
||||
|
||||
Packages compose with each other through named UI injection points (**slots**) and **contributions**.
|
||||
|
||||
### Declaring Slots (Host Surfaces)
|
||||
|
||||
Surfaces declare slots where other extensions can inject UI:
|
||||
|
||||
```json
|
||||
{
|
||||
"slots": {
|
||||
"toolbar-actions": {
|
||||
"description": "Toolbar action buttons",
|
||||
"context": {
|
||||
"noteId": "string — current note ID",
|
||||
"getContent": "function — returns note body text"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Slot names are namespaced at runtime as `{package-id}:{slot-name}` (e.g., `notes:toolbar-actions`). The `context` field documents what data the slot provides — it is not enforced at runtime.
|
||||
|
||||
### Contributing to Slots
|
||||
|
||||
Extensions declare which slots they inject into:
|
||||
|
||||
```json
|
||||
{
|
||||
"contributes": {
|
||||
"notes:toolbar-actions": {
|
||||
"label": "Dictate",
|
||||
"icon": "🎤",
|
||||
"description": "Voice-to-text dictation"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Contributing extensions can be installed before or after their host surface — the coupling is soft. The admin can view all slots and their contributors at `GET /api/v1/admin/slots`.
|
||||
|
||||
### Cross-Package Function Calls
|
||||
|
||||
Any package that declares `exports` can be called via `lib.require()`, not just library-type packages. The caller must declare the target in its `depends` array:
|
||||
|
||||
```json
|
||||
{
|
||||
"depends": ["image-gen"],
|
||||
"permissions": ["api.http"]
|
||||
}
|
||||
```
|
||||
|
||||
## Package Lifecycle
|
||||
|
||||
1. **Install**: Upload a `.pkg` file via Admin > Packages or `POST /api/v1/admin/packages/install`. The kernel extracts the archive, creates database tables, and registers routes.
|
||||
|
||||
@@ -50,7 +50,7 @@ is detected by the kernel. Detected capabilities: `pgvector`, `workspace`,
|
||||
|
||||
### lib
|
||||
|
||||
Load exported functions from library packages.
|
||||
Load exported functions from other packages.
|
||||
|
||||
```python
|
||||
helpers = lib.require("my-utils")
|
||||
@@ -58,11 +58,17 @@ result = helpers.format_date("2026-01-15")
|
||||
```
|
||||
|
||||
Requirements:
|
||||
- The library must be declared in your package manifest's `dependencies`.
|
||||
- The library must be type `library`, status `active`, tier `starlark`.
|
||||
- The target package must be declared in your manifest's `depends` array.
|
||||
- The target package must declare `exports` in its manifest.
|
||||
- The target package must be status `active`, tier `starlark`.
|
||||
- Any package type (`library`, `extension`, `full`) can be called as long
|
||||
as it declares exports. This enables full packages to expose callable
|
||||
functions alongside their UI and API routes.
|
||||
- Circular dependencies are detected and rejected.
|
||||
- Results are cached per execution (calling `require` twice returns the
|
||||
same object).
|
||||
- The called function runs with the *target* package's permissions, not
|
||||
the caller's.
|
||||
|
||||
### permissions
|
||||
|
||||
|
||||
Reference in New Issue
Block a user