cabloy 5.1.152 → 5.1.153

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/.cabloy-version +1 -1
  2. package/.claude/skills/cabloy-module-removal/SKILL.md +1 -1
  3. package/.claude/skills/cabloy-resource-field-update/SKILL.md +1 -1
  4. package/.claude/skills/cabloy-spec-generation/SKILL.md +1 -1
  5. package/.claude/skills/cabloy-spec-generation/references/repo-aware-discovery.md +1 -1
  6. package/.claude/skills/cabloy-workflow/SKILL.md +13 -14
  7. package/.claude/skills/cabloy-workflow/evals/evals.json +4 -4
  8. package/CHANGELOG.md +14 -0
  9. package/CLAUDE.md +2 -3
  10. package/package.json +1 -1
  11. package/repo-docs/.vitepress/config.mjs +5 -0
  12. package/repo-docs/ai/cli-to-skill-map.md +19 -0
  13. package/repo-docs/ai/docs-skills-rules-mapping.md +21 -6
  14. package/repo-docs/ai/future-skill-roadmap.md +17 -72
  15. package/repo-docs/ai/introduction.md +15 -5
  16. package/repo-docs/ai/playbook-spec-execution.md +144 -0
  17. package/repo-docs/ai/playbook-spec-generation.md +155 -0
  18. package/repo-docs/ai/repo-guidance.md +9 -5
  19. package/repo-docs/ai/rules-and-config.md +2 -2
  20. package/repo-docs/ai/skills.md +4 -0
  21. package/repo-docs/ai/virtual-decorator-guidance.md +1 -1
  22. package/repo-docs/backend/introduction.md +1 -1
  23. package/repo-docs/frontend/model-resource-cookbook.md +38 -7
  24. package/repo-docs/frontend/model-resource-usage-guide.md +2 -1
  25. package/repo-docs/frontend/router-tabs-introduction.md +1 -1
  26. package/repo-docs/frontend/use-state-data-best-practices.md +39 -6
  27. package/repo-docs/reference/package-map.md +4 -1
  28. package/repo-docs/reference/repo-scripts.md +25 -0
  29. package/repo-e2e/specs/cabloy-basic.spec.ts +69 -20
  30. package/test-results/.last-run.json +4 -2
  31. package/test-results/cabloy-basic-ATP-BASIC-SUM-58d8a-ry-states-and-Markdown-HTML/error-context.md +195 -0
  32. package/vona/pnpm-lock.yaml +81 -145
  33. package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/payment/controller.tsx +3 -2
  34. package/zova/src/suite/a-training/modules/training-student/src/bean/tableCell.actionSummary.tsx +29 -6
  35. package/zova/src/suite/a-training/modules/training-student/src/config/locale/en-us.ts +1 -0
  36. package/zova/src/suite/a-training/modules/training-student/src/config/locale/zh-cn.ts +1 -0
  37. package/zova/src/suite/a-training/modules/training-student/src/model/student.ts +1 -1
  38. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/package.json +1 -1
  39. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/lib/mutationSuccess.ts +11 -0
  40. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/model/resource.ts +12 -7
  41. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/test/lib/mutationSuccess.test.ts +95 -0
  42. package/zova/src/suite-vendor/a-cabloy/package.json +2 -2
@@ -0,0 +1,155 @@
1
+ # Playbook: Plan a Cabloy Suite Specification
2
+
3
+ Use this playbook to create or maintain the repository-native planning record for a long-lived Cabloy business suite. It explains the public workflow behind the `cabloy-spec-generation` skill without replacing the skill's detailed decision tree.
4
+
5
+ A specification establishes product intent, technical contracts, delivery structure, acceptance procedures, and decision history. It does **not** prove that implementation, generated artifacts, tests, or acceptance evidence already exist.
6
+
7
+ ## When to use this playbook
8
+
9
+ Use `cabloy-spec-generation` when you need to:
10
+
11
+ - establish a new suite's PRD, SRS, WBS, test plan, progress register, and initial ADR
12
+ - extend an existing suite's authoritative planning records
13
+ - turn product intent into traceable delivery and acceptance records
14
+ - prepare an approved handoff for implementation
15
+
16
+ Use a different workflow when:
17
+
18
+ - provider, suite, or module identity is unresolved — use `cabloy-domain-planning`
19
+ - the task is approved backend or frontend scaffolding — use `cabloy-backend-scaffold` or `cabloy-frontend-scaffold`
20
+ - the task is Vona/Zova contract synchronization or generated-consumer drift — use `cabloy-contract-loop`
21
+ - the task is one already approved, bounded WBS increment — use [Playbook: Execute an Approved Cabloy Specification Increment](/ai/playbook-spec-execution)
22
+
23
+ ## Start with repository and edition discovery
24
+
25
+ Before naming a suite, describing source topology, or recommending a site, flavor, UI, SSR, or command path:
26
+
27
+ 1. inspect the active repository root and edition marker
28
+ 2. inspect the root `package.json`, `CLAUDE.md`, relevant CLI entrypoints, and existing suite records
29
+ 3. separate observed source facts from confirmed user inputs, proposed targets, and `TODO(confirm from active source)` items
30
+
31
+ Cabloy Basic and Cabloy Start share the planning model, but their runtime details can differ. Do not carry a Basic command, site identifier, flavor, UI assumption, or generated-output path into Start without confirming it in the active Start source.
32
+
33
+ Read [Edition Detection for AI Workflows](/ai/edition-detection), [Repo Guidance](/ai/repo-guidance), [Repo Scripts](/reference/repo-scripts), and [Package Map](/reference/package-map) before making repository-specific claims.
34
+
35
+ ## Classify the planning request
36
+
37
+ ### New suite baseline
38
+
39
+ A long-lived business domain normally receives the complete planning baseline under `repo-specs/<suite>/`. The specification should be suite-first, name capability modules by business responsibility, and avoid inventing a competing persistence, identity, or authorization owner.
40
+
41
+ ### Existing suite extension
42
+
43
+ Read the existing README and authority map first. Update the owning PRD, SRS, or ADR before downstream WBS, test-plan, progress, or evidence references. Preserve existing identifiers, accepted decisions, history, and evidence conventions rather than replacing them with a fresh parallel record.
44
+
45
+ ### Proportionate planning
46
+
47
+ A disposable demo, tutorial, or isolated utility can use a smaller record only when the requester explicitly selects that scope. Do not reduce the planning baseline for a domain expected to grow merely because its first increment is small.
48
+
49
+ ### Unresolved identity
50
+
51
+ Stop before creating a planning directory when the provider, suite, or capability boundary is unclear. Route the identity decision to `cabloy-domain-planning` rather than creating a competing suite hierarchy.
52
+
53
+ ## Keep the authority chain explicit
54
+
55
+ Repository-native traceability flows in one direction:
56
+
57
+ ```text
58
+ PRD requirement → SRS contract → PDP/WBS task → ATP scenario → observed evidence
59
+ ```
60
+
61
+ Each record has a distinct role:
62
+
63
+ | Record | Authority |
64
+ | ---------------------- | --------------------------------------------------------------------------------------------------------- |
65
+ | `README.md` | Index, reading order, topology summary, and authority map |
66
+ | `prd.md` | Product outcomes, personas, scope, journeys, business rules, and launch criteria |
67
+ | `srs.md` | Technical contracts, ownership, data, tenant, authorization, lifecycle, API, and nonfunctional boundaries |
68
+ | `pdp-wbs.md` | Dependency order, bounded delivery tasks, completion checks, and contract-loop checkpoints |
69
+ | `test-plan.md` | Formal ATP scenarios, procedures, fixtures, expected proof, and release gates |
70
+ | `decisions/*.md` | Durable suite decisions and their alternatives, consequences, and gates |
71
+ | Evidence records | Observed, redacted proof only |
72
+ | `progress.md` | Derived execution status, blockers, evidence links, and next proof |
73
+ | `implementation-*.svg` | Deterministic derived views of planning and progress records |
74
+
75
+ Update the upstream owner before downstream records. For example, a product or technical change belongs in the PRD, SRS, or ADR first; it is then traced into WBS tasks, ATP procedures, progress implications, and derived charts.
76
+
77
+ A proposed ADR is not an accepted decision. Progress cannot introduce a new requirement, contract, or durable boundary, and a chart cannot repair an authority conflict.
78
+
79
+ ## Build the planning baseline after confirmation
80
+
81
+ For a new long-lived suite, the normal core set is:
82
+
83
+ ```text
84
+ repo-specs/<suite>/
85
+ ├── README.md
86
+ ├── prd.md
87
+ ├── srs.md
88
+ ├── pdp-wbs.md
89
+ ├── test-plan.md
90
+ ├── progress.md
91
+ ├── implementation-gantt.svg
92
+ ├── implementation-burndown.svg
93
+ └── decisions/
94
+ └── 0001-*.md
95
+ ```
96
+
97
+ Add presentation contracts, staged rollout records, runbooks, additional ADRs, or an `evidence/` directory only when their scope is justified. Do not create empty evidence records to imply that testing or delivery has begun.
98
+
99
+ Before creating or replacing records, explicitly confirm at least:
100
+
101
+ - repository, edition, suite identity, and target directory
102
+ - intended product outcome, audiences, in-scope capabilities, and deferred scope
103
+ - confirmed and unresolved tenant, authorization, privacy, ownership, lifecycle, migration, and integration constraints
104
+ - site strategy at the level actually supported by current source and confirmed inputs
105
+ - required core documents and justified optional records
106
+ - unresolved decision gates and which WBS branches they block
107
+ - the initial delivery-status policy
108
+
109
+ Silence is not approval. Confirmation to generate records does not accept a durable ADR; retain an unaccepted decision as proposed. Initial delivery rows are normally `not-started`, `deferred`, or explicitly `blocked` unless actual prior evidence was intentionally carried forward.
110
+
111
+ ## Preserve traceability and status integrity
112
+
113
+ Use stable identifiers such as `PRD-*`, `SRS-*`, `WBS-*`, and `ATP-*`. Every referenced exact identifier must resolve to a formal definition in its owning document; a range, wildcard, or prose summary is not a substitute.
114
+
115
+ The status vocabulary is:
116
+
117
+ - `not-started`
118
+ - `in-progress`
119
+ - `implementation-complete`
120
+ - `verified`
121
+ - `blocked`
122
+ - `waived`
123
+ - `deferred`
124
+
125
+ `verified` requires retained, redacted, traceable observed proof. A planning record, a planned command, a generated scaffold, a screenshot, or an unrelated broad check does not automatically provide that proof.
126
+
127
+ ## Generate and check derived charts
128
+
129
+ For a new or substantially revised long-lived specification set whose authoritative Markdown follows the [chart input contract](/reference/repo-scripts#chart-input-contract), refresh the two derived views after changing the README title or language, WBS, test plan, or progress register:
130
+
131
+ ```bash
132
+ npm run spec:charts -- <suite>
133
+ # Use this non-mutating check when verifying an existing chart state:
134
+ npm run spec:charts:check -- <suite>
135
+ ```
136
+
137
+ The generator consumes the suite README, WBS, test plan, and progress register. It emits `implementation-gantt.svg` and `implementation-burndown.svg`; visible chart language follows the suite README. After refreshing charts, use the check when a separate freshness verification is needed, such as CI or a non-mutating review.
138
+
139
+ The check validates the supported chart input contract and generated-view freshness. It does **not** implement a WBS task, run an ATP, establish evidence, prove acceptance, or replace a review of traceability and status semantics. The charts are scope/order views, not schedule forecasts or a new status authority.
140
+
141
+ This current baseline does not require unchanged legacy suite records to be migrated solely to add charts. A legacy record that does not follow the chart input contract must not be treated as chart-compatible until a deliberate record-format normalization aligns its authoritative Markdown. That normalization preserves existing planning authority and does not require an unrelated product or delivery change. Always confirm exact scripts and input expectations in the active repository before running or documenting them for another edition.
142
+
143
+ ## Hand off work without implying implementation
144
+
145
+ Once the planning authority is coherent and implementation is approved, route the next task deliberately:
146
+
147
+ - unresolved domain identity or boundary → `cabloy-domain-planning`
148
+ - approved backend increment → `cabloy-backend-scaffold`
149
+ - approved frontend increment → `cabloy-frontend-scaffold`
150
+ - contract synchronization or generated-consumer drift → `cabloy-contract-loop`
151
+ - an approved bounded WBS increment → [Playbook: Execute an Approved Cabloy Specification Increment](/ai/playbook-spec-execution)
152
+
153
+ Do not make code changes, run initialization or database-reset workflows, deploy, operate a provider, or claim implementation or verification merely because planning records now exist.
154
+
155
+ For the division of responsibility between public docs, skills, repository specs, and internal rationale, read [Docs, Skills, Rules, and CLI Mapping](/ai/docs-skills-rules-mapping).
@@ -8,10 +8,11 @@ Check the root repository signals first:
8
8
 
9
9
  - `package.json`
10
10
  - edition marker files such as `__CABLOY_BASIC__` or `__CABLOY_START__`
11
- - an established internal-documentation home, when present
11
+ - `repo-docs-internal/`
12
+ - `repo-specs/` when the task concerns a suite plan, WBS increment, acceptance record, or evidence
12
13
  - root `.claude/` assets
13
14
 
14
- These surfaces tell the agent which repo it is in and which scripts are canonical. Inspect an internal-documentation home only when the active repository actually provides one.
15
+ These surfaces tell the agent which repo it is in and which scripts are canonical. Inspect relevant `repo-docs-internal/` records as supporting context rather than as a gate to workflow guidance.
15
16
 
16
17
  ## 2. Prefer framework entrypoints over scattered examples
17
18
 
@@ -27,11 +28,14 @@ For frontend workflows:
27
28
 
28
29
  This is more reliable than copying old file structures from examples without understanding the command surface that created them.
29
30
 
30
- ## 3. Use public docs and optional internal notes for different purposes
31
+ ## 3. Use public docs, suite records, and internal notes for different purposes
31
32
 
32
33
  - use `repo-docs/` to explain how people and agents should work
33
- - when an established internal-documentation home exists, use it for maintainer rationale
34
- - when it does not exist, continue from public docs, rules, skills, source, and tests; do not create a new documentation home implicitly
34
+ - use `repo-specs/<suite>/` for suite-local product, technical, delivery, ATP, progress, ADR, and observed-evidence authority
35
+ - use `repo-docs-internal/` for supporting maintainer rationale
36
+ - continue from public docs, rules, skills, source, and tests when a specific internal record is unavailable or irrelevant; do not create a replacement document implicitly
37
+
38
+ For planning work, read the suite README, PRD, SRS, WBS, test plan, and progress records in the order appropriate to the task. `cabloy-spec-generation` maintains planning authority; `cabloy-spec-execution` acts only on a confirmed, bounded WBS increment. Both require active-source and edition discovery before topology or command claims.
35
39
 
36
40
  ## 4. Treat edition detection as mandatory for UI-sensitive work
37
41
 
@@ -8,7 +8,7 @@ Use the root `CLAUDE.md` for concise, durable operational guidance such as:
8
8
 
9
9
  - how the monorepo is organized
10
10
  - where public docs live
11
- - whether an established internal-documentation home is available
11
+ - where `repo-docs-internal/` holds supporting maintainer rationale
12
12
  - which command entrypoints are preferred
13
13
  - why edition detection is mandatory before UI-sensitive guidance or project-creation assumptions
14
14
 
@@ -26,4 +26,4 @@ Use Claude settings for permissions and execution environment, not as the primar
26
26
 
27
27
  ## Documentation boundary
28
28
 
29
- If a rule is important for people and agents to understand, it probably belongs in public docs too. When an established internal-documentation home exists, it may hold maintainer rationale rather than user-facing workflow. Do not infer or create such a home from edition identity alone.
29
+ If a rule is important for people and agents to understand, it probably belongs in public docs too. `repo-docs-internal/` holds supporting maintainer rationale rather than user-facing workflow; individual records may vary by edition.
@@ -42,8 +42,12 @@ Current examples include:
42
42
 
43
43
  - `cabloy-workflow` for choosing the correct Cabloy work path before implementation
44
44
  - `cabloy-domain-planning` for proposing and confirming providerId, suite, and initial module names before scaffolding a new business domain
45
+ - `cabloy-spec-generation` for creating or maintaining suite-local planning authority, traceability, and derived planning views before implementation
46
+ - `cabloy-spec-execution` for coordinating one confirmed WBS increment through specialist implementation, evidence, and derived progress updates
45
47
  - `cabloy-contract-loop` for backend/frontend contract regeneration and drift diagnosis
46
48
  - `cabloy-resource-field-update` for updating an existing backend resource field thread
47
49
  - `cabloy-module-removal` for removing a backend, frontend, or fullstack module cleanly, including generated-runtime cleanup, stale-residue recovery, and verification
48
50
 
51
+ The specification workflows show the same boundary: [Playbook: Plan a Cabloy Suite Specification](/ai/playbook-spec-generation) and [Playbook: Execute an Approved Cabloy Specification Increment](/ai/playbook-spec-execution) explain the durable public workflow, while the skills retain the branching, confirmation gates, specialist routing, and evidence protocol.
52
+
49
53
  The module-removal workflow is a good example of why skills belong in `.claude/skills/` instead of `CLAUDE.md`: the task needs branching, cleanup order, recovery guidance for generated runtime directories such as `vona/.vona` and `zova/.zova`, and a verification checklist that would be too large for a short repo-wide rule.
@@ -201,4 +201,4 @@ Read these pages together:
201
201
  - [Global Bean Lookup](/ai/global-bean-lookup)
202
202
  - [Docs, Skills, Rules, and CLI Mapping](/ai/docs-skills-rules-mapping)
203
203
 
204
- For maintainer-level rationale, consult an established internal-documentation home when the active repository provides one. The public guidance above is the complete shared operational rule.
204
+ For maintainer-level rationale, consult relevant records in `repo-docs-internal/`. The public guidance above is the complete shared operational rule.
@@ -19,7 +19,7 @@ For contributor and automation workflows in this repository, prefer this order:
19
19
  1. inspect the root `package.json` and `npm run vona` entrypoint
20
20
  2. inspect Vona CLI command families such as `create:*`, `init:*`, `tools:*`, and `bin:*`
21
21
  3. inspect the current module or suite layout before creating new files manually
22
- 4. use public docs for user-facing and agent-facing guidance; consult an established internal-documentation home only when the active repository provides one for maintainer rationale
22
+ 4. use public docs for user-facing and agent-facing guidance; consult relevant `repo-docs-internal/` records for supporting maintainer rationale
23
23
 
24
24
  ## Backend reading paths
25
25
 
@@ -87,7 +87,8 @@ export class ModelStudent extends BeanModelBase {
87
87
  return res ?? null;
88
88
  },
89
89
  meta: {
90
- disableSuspenseOnInit: true,
90
+ // The Summary dialog renders query errors itself.
91
+ disableErrorEffect: true,
91
92
  },
92
93
  });
93
94
  }
@@ -121,14 +122,40 @@ const modelStudent = (await ctx.bean._getBean(
121
122
  true,
122
123
  )) as ModelStudent;
123
124
  const querySummary = modelStudent.summary(id);
125
+ await querySummary.suspense();
124
126
 
125
- await querySummary.refetch({ bypassPersister: true });
126
127
  $host.$appModal.dialog({
127
- slotDefault: () => <ZMarkdownHtml html={querySummary.data?.descriptionHtml ?? ''} />,
128
+ slotDefault: () => {
129
+ const hasData = querySummary.data !== undefined;
130
+ const isLoading = !hasData && (querySummary.isPending || querySummary.isFetching);
131
+ const error = querySummary.error;
132
+
133
+ return (
134
+ <>
135
+ {hasData && error && (
136
+ <div class="alert alert-warning" role="alert">
137
+ <span>{this.scope.locale.SummaryRefreshFailed()}</span>
138
+ </div>
139
+ )}
140
+ {error && (
141
+ <div class="alert alert-error" role="alert">
142
+ <span>{error.message}</span>
143
+ </div>
144
+ )}
145
+ {hasData ? (
146
+ <ZMarkdownHtml html={querySummary.data?.descriptionHtml ?? ''} />
147
+ ) : isLoading ? (
148
+ <div role="status">Loading...</div>
149
+ ) : undefined}
150
+ </>
151
+ );
152
+ },
128
153
  });
129
154
  ```
130
155
 
131
- `refetch({ bypassPersister: true })` is useful when this interaction needs an API-fresh result without restoring or scheduling a persistence save through the persister for that fetch. The successful result still updates the model-owned in-memory query, and the dialog remains bound to `querySummary.data`, so the query remains the source of its ongoing render state rather than transferring ownership to an awaited-result snapshot. The option affects only this fetch; normal query cancellation and in-flight deduplication rules still apply. It is not a force-new-request option: when an existing fetch is reused by TanStack Query, that fetch's semantics remain in effect. The bypassed fetch itself does not intentionally replace an existing persisted value; an already queued ordinary persistence callback is a separate operation and is not automatically cancelled. Use static `meta.persister: false` only when persistence should be disabled for the query generally.
156
+ Opening this Summary dialog is an interaction readiness boundary, so the handler awaits the existing model-owned query with `await querySummary.suspense()` before creating the dialog. That wait follows normal query cache, staleness, error, persistence, and deduplication semantics; it is not an unconditional API-fresh request. After opening, the dialog remains bound to reactive query state rather than an awaited one-shot result, so later query updates remain visible.
157
+
158
+ Use `data !== undefined` as the availability boundary. Retained data plus `error` renders two distinct messages while preserving the content: a non-blocking localized refresh-failure warning explains that the content may be outdated, and `error.message` explains the concrete failed fetch. No data plus `error` renders only the concrete fetch error. This is persisted-cache-first stale-while-revalidate UI, not an API-fresh orchestration decision. Whether restore and follow-up revalidation occur depends on data availability, persister configuration, and staleness.
132
159
 
133
160
  ### Avoid
134
161
 
@@ -161,7 +188,8 @@ summary(id: TableIdentity) {
161
188
  return res ?? null;
162
189
  },
163
190
  meta: {
164
- disableSuspenseOnInit: true,
191
+ // The Summary dialog renders query errors itself.
192
+ disableErrorEffect: true,
165
193
  },
166
194
  });
167
195
  }
@@ -242,6 +270,8 @@ deleteForce(id: TableIdentity) {
242
270
  - item/list invalidation remains centralized
243
271
  - the business-facing model exposes semantic actions without competing for cache ownership
244
272
 
273
+ `mutationItem(...)` awaits its standard consistency work before mutation completion: it invalidates matching active `select` queries unless `invalidateSelect: false`, then invalidates matching active queries under the row's item root, and only then invokes and awaits the optional custom `onSuccess`. This keeps `mutateAsync()` aligned with active resource refreshes. `invalidateSelect: false` skips only the default select invalidation; item-root invalidation remains automatic.
274
+
245
275
  ## Recipe 5: customize invalidation for a special mutation
246
276
 
247
277
  ### Use this when
@@ -261,7 +291,6 @@ publish(id: TableIdentity) {
261
291
  },
262
292
  onSuccess: async () => {
263
293
  await this.$$modelResource.$invalidateQueries({ queryKey: ['select'] });
264
- await this.$$modelResource.$invalidateQueries({ queryKey: ['item', id] });
265
294
  await this.$$modelResource.$invalidateQueries({ queryKey: ['select', 'dashboard'] });
266
295
  },
267
296
  });
@@ -271,7 +300,7 @@ publish(id: TableIdentity) {
271
300
  ### Why this works well
272
301
 
273
302
  - the existing resource-owner remains the source of truth for consistency rules
274
- - special cache dependencies stay explicit
303
+ - the automatic item-root invalidation completes before this callback adds the special list dependencies
275
304
  - pages do not need to remember hidden follow-up refetch rules
276
305
 
277
306
  ### Avoid
@@ -412,6 +441,8 @@ batchArchive(ids: TableIdentity[]) {
412
441
  - list invalidation policy remains visible
413
442
  - cache ownership still stays with the existing resource-owner even when row-level helpers are not the right fit
414
443
 
444
+ This uses raw `$useMutationData(...)` because a batch action affects multiple rows rather than the one `id` accepted by `mutationItem(...)`. It does not inherit `mutationItem(...)`'s automatic item-root invalidation or sequencing, so define and await the necessary list and per-item invalidations explicitly.
445
+
415
446
  ## Recipe 10: keep generic blocks working while adding resource semantics
416
447
 
417
448
  ### Use this when
@@ -129,7 +129,8 @@ export class ModelStudent extends BeanModelBase {
129
129
  return res ?? null;
130
130
  },
131
131
  meta: {
132
- disableSuspenseOnInit: true,
132
+ // The Summary dialog renders query errors itself.
133
+ disableErrorEffect: true,
133
134
  },
134
135
  });
135
136
  }
@@ -116,7 +116,7 @@ Recommended order:
116
116
 
117
117
  The public router-tabs docs explain the shared frontend mechanism and how to use it.
118
118
 
119
- For maintainer-only design boundaries, maintenance invariants, and refactor safety rules, consult an established internal-documentation home when the active repository provides one. The public mechanism, layout, route-meta, and Admin/Web comparison guides remain the shared implementation baseline.
119
+ For maintainer-only design boundaries, maintenance invariants, and refactor safety rules, consult relevant `repo-docs-internal/` records. The public mechanism, layout, route-meta, and Admin/Web comparison guides remain the shared implementation baseline.
120
120
 
121
121
  ## See also
122
122
 
@@ -284,6 +284,8 @@ A query established during render remains the owner of its ongoing state. Render
284
284
 
285
285
  An interaction or orchestration boundary may still await `query.refetch()` when it needs one result to decide whether to continue a command, navigate, show a notification, or open a dialog. That result is local to the current sequence; it does not transfer query ownership to the controller or render path.
286
286
 
287
+ When a query-backed dialog needs readiness before it opens, await the existing query's `suspense()` instead. This follows normal query cache, staleness, error, and deduplication semantics; it does not necessarily force an API-fresh request. After the dialog opens, continue to render from the query wrapper rather than the awaited result.
288
+
287
289
  Do not copy an awaited `refetch()` result into a second long-lived controller/render state that drives an open dialog or persistent component. If the UI remains mounted and displays query-backed data, bind it to `query.data` or a model-derived reactive surface so later refetches and model updates remain visible.
288
290
 
289
291
  ### Per-fetch persistence bypass
@@ -305,22 +307,53 @@ await query.refetch({ bypassPersister: true });
305
307
 
306
308
  This option is not a force-new-request flag. Cancellation, `cancelRefetch`, and in-flight deduplication continue to follow TanStack Query semantics. If an existing fetch is reused, its already-established fetch semantics remain in effect. The bypassed fetch itself does not intentionally replace an existing persisted value; an already queued ordinary persistence callback is a separate operation and is not automatically cancelled.
307
309
 
310
+ When the current interaction or polling iteration must make an immediate decision from its API-oriented fetch, consume the awaited result rather than rereading `query.data`:
311
+
312
+ ```ts
313
+ const result = await queryPaymentSession.refetch({
314
+ bypassPersister: true,
315
+ });
316
+ const session = result.data;
317
+
318
+ if (session?.state === 'succeeded') {
319
+ // Decide this iteration from this fetch result.
320
+ }
321
+ ```
322
+
323
+ `query.data` remains the reactive, long-lived surface for rendering and subsequent updates. `result.data` is the result for this one awaited sequence. Keeping those roles separate prevents a controller from treating ongoing render state as the local result variable for a command. The result represents this fetch's response; it does not guarantee that no later server-side state transition occurs, and normal TanStack Query reuse semantics still apply.
324
+
308
325
  This is different from static `meta.persister: false`: the static option disables persistence for the query generally, while `bypassPersister: true` opts out only for one fetch.
309
326
 
310
- For example, an interaction can request a fresh summary and then open a dialog while the dialog remains bound to the query-owned state:
327
+ By contrast, a summary dialog can deliberately use persisted-cache-first behavior while remaining bound to query-owned state:
311
328
 
312
329
  ```ts
313
330
  const querySummary = modelStudent.summary(id);
314
- await querySummary.refetch({ bypassPersister: true });
331
+ await querySummary.suspense();
315
332
 
316
333
  $host.$appModal.dialog({
317
- slotDefault: () => (
318
- <ZMarkdownHtml html={querySummary.data?.descriptionHtml ?? ''} />
319
- ),
334
+ slotDefault: () => {
335
+ const hasData = querySummary.data !== undefined;
336
+ const isLoading = !hasData && (querySummary.isPending || querySummary.isFetching);
337
+ const error = querySummary.error;
338
+
339
+ return (
340
+ <>
341
+ {hasData && error && <SummaryRefreshWarning />}
342
+ {error && <SummaryFetchError error={error} />}
343
+ {hasData ? (
344
+ <ZMarkdownHtml html={querySummary.data?.descriptionHtml ?? ''} />
345
+ ) : isLoading ? (
346
+ <SummaryLoading />
347
+ ) : undefined}
348
+ </>
349
+ );
350
+ },
320
351
  });
321
352
  ```
322
353
 
323
- The awaited result may coordinate the current interaction, but ongoing rendering should continue to read `querySummary.data`.
354
+ This Summary interaction deliberately awaits `querySummary.suspense()` before opening the dialog. It therefore has a query-readiness boundary, but does not force an API-fresh request: the wait still follows normal query cache, staleness, error, persistence, and deduplication semantics. The open dialog reads every later transition through `querySummary.data` and its query status rather than taking ownership of an awaited one-shot result.
355
+
356
+ Treat `data !== undefined` as the availability boundary. Retained data plus `error` has two separate user-facing meanings: render a non-blocking refresh-failure warning to explain that the retained content may be outdated, render the concrete `error.message` through `SummaryFetchError`, and keep the content visible. With no data plus `error`, render only the concrete fetch error. This is persisted-cache-first stale-while-revalidate UI, not an API-fresh orchestration request. Restore and follow-up revalidation depend on data availability, persister configuration, and staleness.
324
357
 
325
358
  ## Practical rule 7: derive render-time state once per render when possible
326
359
 
@@ -6,9 +6,12 @@ This monorepo is organized around a small set of stable top-level areas.
6
6
 
7
7
  - `package.json` — shared scripts and release entrypoints
8
8
  - `lerna.json` — package topology
9
- - an established internal-documentation home, when present — edition-local maintainer notes and ADRs
9
+ - `repo-docs-internal/` — edition-local maintainer notes and ADRs
10
10
  - `.claude/` — Claude commands, skills, and local settings
11
11
  - `repo-docs/` — unified public docs
12
+ - `repo-specs/` — suite-local product, technical, delivery, ATP, progress, observed-evidence, and ADR authority
13
+
14
+ `repo-specs/` is distinct from `repo-docs/`, which explains reusable public guidance, `.claude/`, which contains executable workflow behavior, and `repo-docs-internal/`, which preserves supporting maintainer rationale. For the public planning workflow, see [Playbook: Plan a Cabloy Suite Specification](/ai/playbook-spec-generation).
12
15
 
13
16
  ## Backend side
14
17
 
@@ -42,6 +42,31 @@ Cabloy Basic exposes these shared root scripts:
42
42
 
43
43
  `npm run init` prepares all Cabloy Basic SSR and REST artifacts with `npm run build:zova:all`, which sequentially builds the Basic and Commerce flavor batches before Vona initialization. Use `build:zova` or `build:zova:commerce` for focused artifact refreshes; use `build:zova:all` only when every shipped Basic flavor must be prepared.
44
44
 
45
+ ## Specification planning and derived charts
46
+
47
+ The current Cabloy Basic root scripts also expose:
48
+
49
+ ```bash
50
+ npm run test:spec-charts
51
+ npm run spec:charts -- <suite>
52
+ npm run spec:charts:check -- <suite>
53
+ ```
54
+
55
+ `spec:charts` refreshes the generated Gantt and burndown SVG views for a chart-compatible `repo-specs/<suite>/` record. `spec:charts:check` validates the supported input contract and detects stale generated views; `test:spec-charts` runs the chart-tool test suite.
56
+
57
+ ### Chart input contract
58
+
59
+ The generator consumes `README.md`, `pdp-wbs.md`, `test-plan.md`, and `progress.md`. The supported Markdown format includes:
60
+
61
+ - formal `### Phase <number>:` and `#### WBS-...:` headings in the WBS, with supported dependency labels
62
+ - formally defined `ATP-*` scenarios in the test plan for every ATP reference used by a WBS task
63
+ - one progress row for each WBS item, with the WBS ID in the first cell and its supported status in the second cell
64
+ - a README whose current title and language should be reflected by regenerated chart output
65
+
66
+ A legacy suite with a different WBS or progress-table layout is not chart-compatible until a deliberate record-format normalization aligns its authoritative Markdown with this input contract. Format normalization must preserve the existing planning authority; it does not require an unrelated product or delivery change.
67
+
68
+ These commands do not create planning authority, implement a WBS task, execute an ATP, produce acceptance evidence, or replace traceability/status review. Confirm the active root `package.json` and script input expectations before assuming equivalent behavior in Cabloy Start or another repository.
69
+
45
70
  ## Cabloy Start entrypoints
46
71
 
47
72
  Cabloy Start exposes the equivalent Start repository surface:
@@ -414,7 +414,7 @@ test(
414
414
  );
415
415
 
416
416
  test(
417
- 'ATP-BASIC-SUMMARY-01: Training Student summary renders Markdown HTML in a dialog',
417
+ 'ATP-BASIC-SUMMARY-01: Training Student summary dialog renders query states and Markdown HTML',
418
418
  { tag: ['@admin', '@flow'] },
419
419
  async ({ page }) => {
420
420
  const pageErrors = collectPageErrors(page);
@@ -466,25 +466,74 @@ test(
466
466
  await page.getByRole('button', { name: 'Search', exact: true }).click();
467
467
  await searchResponse;
468
468
 
469
- const row = page.locator('tr').filter({ hasText: studentName });
470
- await expect(row).toHaveCount(1);
471
- const summaryResponse = page.waitForResponse(response => {
472
- const url = new URL(response.url());
473
- return (
474
- response.request().method() === 'GET' &&
475
- response.ok() &&
476
- url.pathname === `/api/training/student/summary/${studentId}` &&
477
- !response.request().headers()['x-vona-openapi-schema']
478
- );
479
- });
480
- await row.getByRole('button', { name: 'Summary', exact: true }).click();
481
- await summaryResponse;
482
-
483
- const description = page.locator('.student-summary-description');
484
- await expect(description).toBeVisible();
485
- await expect(description.locator('h2')).toHaveText('Summary heading');
486
- await expect(description).toContainText(`Summary paragraph ${studentName}`);
487
- expect(pageErrors).toEqual([]);
469
+ const summaryPath = `/api/training/student/summary/${studentId}`;
470
+ let summaryMode: 'failure' | 'success' = 'failure';
471
+ let releaseSummaryRequest: (() => void) | undefined;
472
+ let summaryRequest: Promise<void> | undefined;
473
+ let releaseSummaryResponse: (() => void) | undefined;
474
+ let summaryResponseRelease: Promise<void> | undefined;
475
+ const summaryHandler = async (route: Route) => {
476
+ releaseSummaryRequest?.();
477
+ if (summaryMode === 'failure') {
478
+ await route.fulfill({
479
+ status: 500,
480
+ contentType: 'application/json',
481
+ body: JSON.stringify({ code: 500, message: 'Summary temporarily unavailable' }),
482
+ });
483
+ return;
484
+ }
485
+ const response = await route.fetch();
486
+ await summaryResponseRelease;
487
+ await route.fulfill({ response });
488
+ };
489
+ const summaryRouteUrl = (url: URL) => url.pathname === summaryPath;
490
+ await page.route(summaryRouteUrl, summaryHandler);
491
+ try {
492
+ let row = page.locator('tr').filter({ hasText: studentName });
493
+ await expect(row).toHaveCount(1);
494
+ await row.getByRole('button', { name: 'Summary', exact: true }).click();
495
+
496
+ const dialog = page.getByRole('dialog');
497
+ await expect(dialog).toBeVisible();
498
+ await expect(dialog.locator('.alert.alert-error')).toHaveCount(1);
499
+ await expect(dialog.locator('.student-summary-description')).toHaveCount(0);
500
+ await page.keyboard.press('Escape');
501
+ await expect(dialog).toHaveCount(0);
502
+
503
+ summaryMode = 'success';
504
+ summaryRequest = new Promise(resolve => {
505
+ releaseSummaryRequest = resolve;
506
+ });
507
+ summaryResponseRelease = new Promise(resolve => {
508
+ releaseSummaryResponse = resolve;
509
+ });
510
+ const listResponse = await page.goto('/admin/rest/resource/training-student%3Astudent', {
511
+ waitUntil: 'load',
512
+ });
513
+ expect(listResponse?.ok()).toBeTruthy();
514
+ await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'admin');
515
+ await page.getByLabel('Student Name').fill(studentName);
516
+ const nextSearchResponse = waitForStudentSelect(page);
517
+ await page.getByRole('button', { name: 'Search', exact: true }).click();
518
+ await nextSearchResponse;
519
+
520
+ row = page.locator('tr').filter({ hasText: studentName });
521
+ await expect(row).toHaveCount(1);
522
+ await row.getByRole('button', { name: 'Summary', exact: true }).click();
523
+ await summaryRequest;
524
+ await expect(dialog).toHaveCount(0, { timeout: 250 });
525
+ releaseSummaryResponse?.();
526
+
527
+ const description = dialog.locator('.student-summary-description');
528
+ await expect(dialog).toBeVisible();
529
+ await expect(description).toBeVisible();
530
+ await expect(description.locator('h2')).toHaveText('Summary heading');
531
+ await expect(description).toContainText(`Summary paragraph ${studentName}`);
532
+ expect(pageErrors).toEqual([]);
533
+ } finally {
534
+ releaseSummaryResponse?.();
535
+ await page.unroute(summaryRouteUrl, summaryHandler);
536
+ }
488
537
  } finally {
489
538
  try {
490
539
  if (studentId !== undefined) {
@@ -1,4 +1,6 @@
1
1
  {
2
- "status": "passed",
3
- "failedTests": []
2
+ "status": "failed",
3
+ "failedTests": [
4
+ "0d2518050eca8cd1231a-03158dd238416d7c8c3e"
5
+ ]
4
6
  }