Merge remote-tracking branch 'educraft/main' into merge/educraft-cph

# Conflicts:
#	.gitignore
#	hub/.env.example
#	hub/deploy/deploy_fleet_release.sh
#	hub/deploy/deploy_platform.sh
#	hub/test/integration/helpers.ts
This commit is contained in:
2026-08-06 00:49:02 +08:00
262 changed files with 10630 additions and 1377 deletions
+81
View File
@@ -0,0 +1,81 @@
# ADR 0028: Agent Configuration Folder Tree
## Status
Accepted.
## Context
ADR-0017/0018 made Agent roles and skills Organization-scoped dynamic runtime
configuration, managed without process restarts. The org admin surfaces for
them (`/admin/roles` and `/admin/skills`) render every
role/skill as one large editor card in a single flat list. As an Organization
accumulates roles and skills, the management pages degrade into an endless
scroll with no grouping affordance.
The project explorer already solves the analogous problem for projects with
transparent folders (ADR-0021): org-scoped navigation nodes that are not
permission resources. Roles and skills need the same affordance, but their
semantics differ from projects in one crucial way: skill names and role IDs
are referenced by role→skill bindings, run-time skill snapshot loading, and
Feishu slash commands. Any grouping mechanism must not leak into those
resolution paths.
## Decision
Introduce an Organization-scoped folder tree shared by Agent roles and Agent
skills:
- One folder tree per Organization is shared by both roles and skills (e.g. a
"高三化学组" folder groups that team's roles and its skills together). It is
a distinct entity from the project explorer `Folder` of ADR-0021 — the two
trees are managed independently and never reference each other.
- Folders are **transparent organization nodes**, following the ADR-0021
project-folder precedent: they exist for management-surface navigation and
grouping only, are not permission resources, and hold no grants.
- Folder membership is **not part of role/skill identity or resolution**:
- skill `name` and role `roleId` remain unique per Organization regardless
of folder membership;
- role→skill bindings, run admission's frozen role snapshot, run-scoped
skill loading, and Feishu slash commands never reference folders.
- Each role/skill sits in at most one folder; membership is optional
(unfiled items remain first-class). Folders nest arbitrarily.
- Folder assignment is a label-class change in the ADR-0017 sense: it never
archives Agent sessions, because the execution surface (model, prompt,
tools, skill content) is untouched.
- A folder can be deleted only when empty — no child folders, no roles, no
skills. Relocating items out of a folder is an explicit user action, so no
orphan-placement rule is needed yet.
- Admin web renders both pages as a left folder tree plus the item list of
the selected folder ("all" and "unfiled" included). The host-console CLI is
unchanged: folder management lives in the web surface, and CLI
`upsert-role`/`install-skill` never touch folder assignment.
## Consequences
- New DB entity `OrganizationAgentConfigFolder` (org-scoped, self-nesting via
`parentId`, delete restricted while referenced) plus nullable `folderId` on
`OrganizationAgentRole` and `OrganizationAgentSkill` (SetNull on folder
delete, though the service refuses to delete non-empty folders).
- The spec pins the transparency and single-membership semantics in
`Spec.System.AgentRole` (`AgentConfigFolder`), so future implementors do
not re-derive them differently (e.g. path-style names or per-folder
uniqueness).
- Org admin APIs gain folder CRUD plus role/skill folder-assignment endpoints
that skip session archival by construction.
- Moving a role/skill between folders changes nothing about authorization,
run resolution, or audit-visible configuration lineage beyond the folder
assignment event itself.
## Open Questions / Deferred
- Drag-and-drop assignment and bulk moves are deferred; assignment is a
per-item select for now.
- Folder-level usage aggregation for roles/skills is deferred (project
folders already aggregate usage under ADR-0021; agent configuration has no
usage dimension yet).
- CLI flags for folder assignment are deferred until a console workflow asks
for them.
- Folder-scoped default-role policies (e.g. per-folder defaults) are rejected
for now: the Organization keeps exactly one active default role
(ADR-0017/0018 invariant) regardless of folder structure.
@@ -0,0 +1,193 @@
# ADR 0029: Engineering-File Structure Is A Nested Outline Manifest
## Status
Accepted. **Supersedes ADR-0008** on the concrete layout of a lesson's
structure (the ordered `[[parts]]` arrangement), and discharges the
"grouping/sectioning" and "manifest richness" gaps ADR-0008 left Open. ADR-0007
(the engineering file is a real directory tree) stands unchanged. ADR-0005's
"a lesson is an ordered sequence of element instances" stands unchanged — order
and membership are preserved; the *shape* that encodes them is now a tree.
## Context
ADR-0008 encodes a lesson's order and membership as a **single flat `[[parts]]`
array** in a root `manifest.toml`, where every `[[parts]]` entry is a `kind` +
`path` to an element folder. Two forces now push against that flat shape:
1. **Real lessons are internally structured.** TH-144 has a `题目/` tree of
problem/answer pairs and A/B/C sections that exist only in folder names
today. Teachers think of a lesson as an **outline** — sections, sub-sections,
groups of worked examples — not an unbroken flat list of ~40 parts. The
admin/teacher surface (the 老师端 being built against the Hub) is supposed to
show "the project structure, expanding each structural element to the files
inside" — and today that structure is a giant flat scroll.
2. **Outline and file structure should correspond 1:1, not via a separate
index.** The 7.31 design discussion landed on a shape where each level of the
lesson is a folder whose manifest states that level's children — so the
on-disk tree *is* the outline, self-descriptive, with no secondary artifact
to drift out of sync. A flat root `[[parts]]` list, by contrast, names the
whole lesson in one file and forces the folder tree to be a projection of it
(or vice versa) with two sources of truth.
ADR-0008 itself anticipated this: its Open Questions list "Per-part metadata,
grouping/sectioning (TH-144's A/B/C structure is only in folder names today)" as
explicitly not modeled. This ADR closes that gap.
## Decision
### A lesson is a tree of folders; each folder is a self-describing node whose manifest names its children
The engineering file remains a real directory tree (ADR-0007). The ordering and
membership encoding of ADR-0008 changes from **one flat root `[[parts]]`** to a
**nested, per-folder outline**:
- The root's `manifest.toml` keeps `[project]`, `[info]`, and the `[targets.*]`
build configuration exactly as ADR-0008/0011 define them.
- The lesson's **structure is expressed as a folder tree**, where every folder
that groups children carries its own small **outline manifest** (per-folder
`manifest.toml`, see *Name and discriminator* below) stating that level's
ordered children.
- A **leaf** is an element folder exactly as ADR-0008 defines it: an
`element.toml` declaring `kind` + scalar fields, plus convention-named
content `.typ` siblings. A leaf has no outline manifest — its own
`element.toml` is its descriptor.
- An **internal folder** (a grouping node) has an outline manifest but no
`element.toml`: it is not an element, it is a container of elements/containers.
It carries only structure and, optionally, group-level scalar metadata.
### The engineering-file root is itself the implicit top container
Children live directly in the root `manifest.toml`'s `children` array — there is
no mandated single top-level section folder. Rationale: migration is a pure
flatten of the existing root `[[parts]]` into root `children` (same order, zero
forcing); a mandated wrapper folder would be pure indirection for most lessons.
A lesson that wants a top-level section simply creates one as a child
(consistent with the ADR-0021 folder-tree precedent, which holds direct children
at the root).
### Name and discriminator: every grouping folder uses `manifest.toml`
Every folder that groups children uses the same filename, `manifest.toml`, at
every level including the root:
- Root `manifest.toml`: `[project]`, `[info]`, `[targets.*]`, plus a `children`
array.
- Internal folder `manifest.toml`: `children` (+ optional `[group]` scalars);
never `[project]`/`[info]`/`[targets.*]`.
The leaf/container discriminator is disjoint and structural: a folder with
`element.toml` is a **leaf** (ADR-0008 descriptor); a folder with `manifest.toml`
and no `element.toml` is a **container**; a folder with neither is a structural
error. `OUTLINE.toml` was rejected (a new reserved name, no benefit over the
uniform name); `info.toml` was rejected because it collides with the model's
`Info` (title/author, folded into root `manifest.toml`'s `[info]` by ADR-0008)
and would blur "metadata vs structure". The 7.31 sketch's intent — each level
self-describes its children — is preserved; the name aligns with current
ADR-0008.
### Order is encoded per-folder, and the lesson order is the depth-first traversal
ADR-0005 requires the lesson to be an ordered sequence. In the tree, **order is
declared locally at each folder** by the order of children in that folder's
`manifest.toml`. The canonical lesson order is the **depth-first pre-order
traversal** of the tree: an internal folder contributes no element of its own
(its label is a heading, not a part), and leaves contribute in the order they
appear. The checker materializes this traversal; no part of the lesson order
lives in a typst script (ADR-0008's core rejection of typst-as-order-manifest
stands).
Concretely, a `segment` that in TH-141 was one flat `[[parts]]` entry can now be
a folder whose outline lists its sub-segments and examples in order — and any
grouping (TH-144's A/B/C, a "导言 cluster", a "例题组") is a folder, transparent
in the element sequence but a real node in the outline.
### The outline manifest shape
A folder's `manifest.toml` `children` array holds its ordered children. Each
child entry is either:
```toml
# a leaf element (ADR-0008 descriptor), by relative path
{ kind = "example", path = "examples/41届复赛三-1-混注石油" }
# or an internal grouping folder, by relative path (recursed)
{ kind = "section", path = "导言簇" }
```
The `kind` of a **leaf** is still read from that folder's `element.toml`
(ADR-0008: the folder is self-describing; the outline entry may restate it for
readability but the `element.toml` is authoritative). The `kind` of an
**internal** child is a container kind — recognized from a small, open set of
container kinds — and selects how that subtree is rendered/grouped. Leaves and
containers are disjoint by construction: a folder is a container iff it has a
`manifest.toml`; a leaf iff it has an `element.toml`. A folder must have exactly
one of the two.
### Container kinds: MVP ships exactly one — `section`, rendered as a heading
The demonstrated needs (TH-144's A/B/C, a "导言簇") are all `section`, so the
MVP ships exactly one container kind:
- A `section` opens a **heading** at its depth in the DFS, then renders its
children in order; it never appears in the element part sequence (the
already-decided DFS semantics).
- The heading uses `[group].title` when present, else the folder name.
- `group` (a heading-less visual grouping) and any other container kind are
**deferred**: added only when a real need appears, honoring ADR-0005's open
universe / "add when needed".
### Group-level scalars
An internal folder MAY carry a `[group]` table (e.g. a title distinct from the
folder name, a description) in its `manifest.toml`. Kept minimal — no other
container metadata until a real need appears.
### The flat `[[parts]]` array at the root is retired for structure
The root `manifest.toml` no longer needs a root-level `[[parts]]` that names the
whole lesson. Lesson structure lives in the folder tree, rooted at the
engineering-file root's `manifest.toml` `children`. `[targets.*]` and
`[project]`/`[info]` stay at the root `manifest.toml`.
## Consequences
- The teacher/admin surface shows the outline: the folder tree *is* the lesson
structure, self-descriptive and 1:1 with files. Opening an element reveals its
files (as the 老师端 requirement asked). This discharges the 7.31 driver
("项目内部有一套 cph schema 定义的结构,由 manifest 组织,给老师看的应该是这个,
展开每个结构元素内部才是文件").
- The checker can still recover the full ordered lesson **without evaluating
typst**: it reads the root `manifest.toml` (project/info/targets) and walks the
folder tree, honoring each folder's `manifest.toml` children order and each
leaf's `element.toml`. Order and membership remain declarative data, greppable
and diffable.
- Grouping (sections) is now a real, checkable structure rather than a folder
naming convention — TH-144's A/B/C can be first-class.
- Every folder is self-describing, so a subtree can be understood/moved on its
own; nothing about a subtree's structure lives only in the root file.
- Migration is mechanical: flatten the existing root `[[parts]]` into root
`children` with the same leaf order (root is the implicit top container, so no
wrapper folder is needed). The element sequence is unchanged, so
`cph check`/`cph build` semantics for leaves carry over.
## Open Questions / Deferred
- **Target-scoped container options** (e.g. hide a section in the student
build): deferred, stay out of structure. ADR-0009/0011 field-visibility /
per-target map already handles this in rendered output, not structure; keep it
there unless a concrete need forces it back into the manifest.
- **Additional container kinds** beyond `section` (e.g. a heading-less `group`):
deferred until a real need appears (open universe, ADR-0005).
- **Container metadata beyond `[group]`** (title/description): deferred — only
the minimal `[group]` table ships; richer container scalars await a concrete
authoring need.
## Supersedes
ADR-0008's "the ordering manifest is declarative" decision stands; this ADR
replaces its **flat `[[parts]]` encoding of order/membership** with the nested
per-folder outline. ADR-0008's other decisions (declarative `manifest.toml`/
`element.toml`, folder self-description, content-file naming convention, schema
as source of truth for which `.typ` files exist) are unchanged and carry into
this tree form.
+167
View File
@@ -0,0 +1,167 @@
# ADR 0030: Batch & Combined Export
## Status
Accepted. **Extends/refines ADR-0009 and ADR-0011** (export target = a build
producing a typed artifact) by adding **two** export dimensions that today have
no home: (1) building **multiple targets of one lesson** in one batch, and (2)
**combining multiple lessons into one** artifact — a 讲义合集 / course bundle.
It does **not** redefine the SingleFile vs FileTree artifact distinction
(ADR-0011) or the single-`typstCompile`-step MVP; it adds the *collection*
semantics on top.
## Context
Today `cph build --target T` builds exactly one target `T` of one engineering
file into one artifact. Two real needs fall outside that:
1. **A lesson's multiple versions.** A lesson already declares several targets
(student handout, teacher plan, slides, script). Producing all of them is
today N separate `cph build` invocations with no shared invocation, ordering,
or failure summary. Teachers preparing a lesson want "build the whole lesson
in all needed forms" as one action.
2. **Combining lessons into one deliverable.** ADR-0005 deferred "course =
arrangement of lessons" — a course/unit is *not* an engineering file; it is
an arrangement of lessons "modeled elsewhere". The elsewhere is empty. A real
deliverable is a **讲义合集 / course bundle** — several lessons ordered into
one document (e.g. "期中复习合集", a term bundle, a topic compilation). This
spans multiple engineering files and currently has no model and no CLI path.
Both are product-plain features (the 老师端 exports; a bundle is what a teacher
hands a class), not architectural speculation.
## Decision
### The existing single-lesson single-target build is the atomic unit
ADR-0009/0011's model — a target is a build over one lesson producing one
`Artifact` via ordered `Step`s — is unchanged and remains the *unit*. Nothing
below replaces it; the new semantics are **aggregations over that unit**.
### Dimension 1 — Batch: build a set of targets of one lesson
`cph build` on a lesson gains the ability to produce **several targets in one
invocation**, as one batched operation:
- The lesson root `manifest.toml` `[targets.*]` already enumerates the declared
targets and their order (ADR-0008/0011). Building "all declared targets" is the
default batch: each declared target builds to its own artifact
(`build/<target>.{pdf,md}`), in declaration order.
- A batched build is **non-transactional and independent per target**: each
target is a separate build with its own artifact, own diagnostics, own
exit/result. One target failing (e.g. teacher plan PDF) does not block the rest
(student PDF), matching the per-target independent-failure stance of
ADR-0009's "missing render for a used kind ⇒ warning, non-blocking".
- The batch emits a **summary**: a per-target ledger (ok/failed + its artifact
or error), and a **non-zero aggregate exit if any target failed** to produce
its artifact. A target that fails to produce its artifact is a real defect
(this repo's fail-fast stance — don't paper over bugs), distinct from
ADR-0009's "missing render rule ⇒ warning" (a policy-level skip, non-error).
"Don't block the rest" still holds: every target is attempted, but a single
failure makes the aggregate non-zero so CI/observability catch it. This is the
CLI's job; it is the natural "build the whole lesson" affordance.
### Dimension 2 — Combined: arrange multiple lessons into one artifact
A **course bundle** is a new, lightweight, second kind of engineering-file-adjacent
unit: an **ordered arrangement of lessons** (ADR-0005's deferred "course =
arrangement of lessons" finally given a concrete export home).
#### Bundle carrier: a directory containing `bundle.toml`
A bundle is a **directory containing `bundle.toml`**. A directory gives the
bundle a stable root for relative lesson paths and a home for build output
(echoing ADR-0007's "engineering file = directory"). The `bundle.toml` carries:
- `[info]` — the bundle's own title/author (of the 合集);
- `[targets.*]` — the bundle's build configuration, reusing ADR-0011's build
mechanism;
- an ordered `lessons` array — each entry: a lesson path (relative to the
bundle root, pointing at each engineering-file root, each a self-contained
directory tree per ADR-0007) plus optional per-lesson per-target overrides.
A bundle target produces an **ordered concatenation/assembly of the lessons'
artifacts or content** into one `Artifact`, reusing the ADR-0011 artifact ADT:
- `SingleFile` — a combined document (讲义合集): the lessons' content assembled
in order into one compiled document, with the existing cross-reference /
`@label` machinery working because it is one compiled document (the same
reason ADR-0011 gives for why SingleFile concatenation works at all).
- `FileTree` — each lesson to its own file plus a generated index (ADR-0011's
third-party-archive case, now with a first-class multi-lesson trigger).
A bundle target's steps are the **ordered typed steps** of ADR-0011, but the
"map" now operates at the level of whole lessons rather than a single lesson's
parts: a step like `assembleLessons` (ordered inclusion of each lesson's
content/artifact) plus the existing `typstCompile`/`shell` steps for assembly
and any post-processing. Concretely the framework provides a
`typstCompile`-style step that pulls each listed lesson's content in order into
one document (mirroring how a single lesson's template pulls its parts).
#### Renumbering in a `SingleFile` bundle: template-resident, default reset per lesson
Numbering is presentation, which ADR-0011 already owns to the template file (not
the manifest), so the **bundle target's template decides** whether auto-counters
reset at lesson boundaries; the framework ships a helper to reset counters at a
lesson boundary. The **recommended default resets auto-counters at each lesson
boundary**: lessons are authored self-contained, so an internal "例题3" means
that lesson's 例题3; cross-lesson continuation would silently break author
references. A genuine "全书 continuous numbering" is an explicit template
override. `@label` cross-references stay global (resolved by label name,
independent of counters); only auto-increment counters reset.
#### Bundles do not nest (MVP)
A bundle references lessons only, not other bundles, until a real need appears —
mirrors the tree-nesting simplicity and keeps the first bundle target minimal.
### Invariant: lessons stay self-contained; combination is export-time only
Opening the door to combining lessons must **not** open the door to cross-lesson
imports inside a lesson's own content (ADR-0006's import boundary: within one
engineering file plus `@package`, never into a sibling lesson). A bundle is
allowed to *assemble already-authored lessons at export time* — reading their
content for the combined artifact — but no lesson's rich content may `import`
another lesson's internals as part of *its own* authoring. Combination is a
**projection over self-contained lessons**, exactly as a render target is a
projection over a lesson. This keeps each engineering file independently
checkable, buildable, and movable, and avoids reintroducing cross-file coupling
ADR-0006 explicitly rejected.
### CLI surface
- `cph build <lesson>` → all declared targets (the batch default).
- `cph build <lesson> --target student --target teacher` → the named subset
(the multi-version batch), in the given order.
- `cph bundle <bundle-path> --target <name>` → build one combined bundle target
(`SingleFile` merged doc or `FileTree`), giving the multi-lesson merge.
(`cph build` on a bundle root is the batch-of-bundle-targets equivalent.)
## Consequences
- **One lesson, many versions** is one command with a per-target ledger — the
natural 老师端 "导出全部版本" action, and any single failure surfaces as a
non-zero aggregate.
- **Course = arrangement of lessons** gets a concrete, export-focused home (the
bundle), discharging the ADR-0005 deferred item without inventing a full
course-authoring model.
- The **artifact/distinction and build-step machinery (ADR-0011) is reused** — a
bundle target is just a build whose inputs are whole lessons, not a new
parallel export engine.
- **Self-containment stays** (ADR-0006/0007): each lesson remains independently
checkable and buildable; the bundle only reads them for assembly. A lesson and
a bundle can version/evolve independently.
- The teacher surface can offer "export all versions" (batch) and "compile into
a 合集" (combined) as two concrete, productisible actions.
## Open Questions / Deferred
- **Bundle-of-bundles / nesting** — no nesting in MVP; re-open only when a real
need appears.
- **Dedup/caching across targets and lessons** — none in MVP, consistent with
ADR-0011's "no caching in MVP".
- **Exact counter-reset semantics in a `SingleFile` bundle** — the default
(reset per lesson) is decided; the precise mechanism (which counters, how the
template override is expressed) is settled with the first bundle template
implementation.