@event4u/agent-config 1.13.0 → 1.15.0
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.
- package/.agent-src/commands/agent-handoff.md +4 -1
- package/.agent-src/commands/agent-status.md +3 -0
- package/.agent-src/commands/agents-audit.md +4 -0
- package/.agent-src/commands/agents-cleanup.md +6 -1
- package/.agent-src/commands/agents-prepare.md +3 -0
- package/.agent-src/commands/analyze-reference-repo.md +4 -0
- package/.agent-src/commands/bug-fix.md +7 -3
- package/.agent-src/commands/bug-investigate.md +4 -0
- package/.agent-src/commands/chat-history-checkpoint.md +126 -0
- package/.agent-src/commands/chat-history-clear.md +6 -1
- package/.agent-src/commands/chat-history-resume.md +7 -2
- package/.agent-src/commands/chat-history.md +7 -2
- package/.agent-src/commands/check-current-md.md +137 -0
- package/.agent-src/commands/commit-in-chunks.md +118 -0
- package/.agent-src/commands/commit.md +4 -0
- package/.agent-src/commands/compress.md +37 -2
- package/.agent-src/commands/context-create.md +4 -0
- package/.agent-src/commands/context-refactor.md +4 -0
- package/.agent-src/commands/copilot-agents-init.md +3 -0
- package/.agent-src/commands/copilot-agents-optimize.md +3 -0
- package/.agent-src/commands/create-pr-description.md +4 -0
- package/.agent-src/commands/create-pr.md +4 -0
- package/.agent-src/commands/do-and-judge.md +4 -1
- package/.agent-src/commands/do-in-steps.md +3 -0
- package/.agent-src/commands/e2e-heal.md +4 -0
- package/.agent-src/commands/e2e-plan.md +4 -0
- package/.agent-src/commands/estimate-ticket.md +4 -1
- package/.agent-src/commands/feature-dev.md +4 -0
- package/.agent-src/commands/feature-explore.md +4 -0
- package/.agent-src/commands/feature-plan.md +4 -0
- package/.agent-src/commands/feature-refactor.md +4 -0
- package/.agent-src/commands/feature-roadmap.md +6 -0
- package/.agent-src/commands/fix-ci.md +4 -0
- package/.agent-src/commands/fix-portability.md +5 -2
- package/.agent-src/commands/fix-pr-bot-comments.md +4 -0
- package/.agent-src/commands/fix-pr-comments.md +4 -0
- package/.agent-src/commands/fix-pr-developer-comments.md +4 -0
- package/.agent-src/commands/fix-references.md +3 -0
- package/.agent-src/commands/fix-seeder.md +4 -0
- package/.agent-src/commands/implement-ticket.md +39 -13
- package/.agent-src/commands/jira-ticket.md +4 -0
- package/.agent-src/commands/judge.md +3 -0
- package/.agent-src/commands/memory-add.md +5 -3
- package/.agent-src/commands/memory-full.md +5 -2
- package/.agent-src/commands/memory-promote.md +7 -6
- package/.agent-src/commands/mode.md +3 -0
- package/.agent-src/commands/module-create.md +4 -0
- package/.agent-src/commands/module-explore.md +4 -0
- package/.agent-src/commands/onboard.md +33 -0
- package/.agent-src/commands/optimize-agents.md +4 -0
- package/.agent-src/commands/optimize-augmentignore.md +12 -0
- package/.agent-src/commands/optimize-rtk-filters.md +3 -0
- package/.agent-src/commands/optimize-skills.md +4 -0
- package/.agent-src/commands/override-create.md +4 -0
- package/.agent-src/commands/override-manage.md +4 -0
- package/.agent-src/commands/package-reset.md +3 -0
- package/.agent-src/commands/package-test.md +3 -0
- package/.agent-src/commands/prepare-for-review.md +4 -0
- package/.agent-src/commands/project-analyze.md +4 -0
- package/.agent-src/commands/project-health.md +4 -0
- package/.agent-src/commands/propose-memory.md +6 -8
- package/.agent-src/commands/quality-fix.md +4 -0
- package/.agent-src/commands/refine-ticket.md +12 -7
- package/.agent-src/commands/review-changes.md +39 -8
- package/.agent-src/commands/review-routing.md +4 -0
- package/.agent-src/commands/roadmap-create.md +18 -0
- package/.agent-src/commands/roadmap-execute.md +14 -1
- package/.agent-src/commands/rule-compliance-audit.md +4 -0
- package/.agent-src/commands/set-cost-profile.md +11 -0
- package/.agent-src/commands/sync-agent-settings.md +12 -0
- package/.agent-src/commands/sync-gitignore.md +3 -0
- package/.agent-src/commands/tests-create.md +4 -0
- package/.agent-src/commands/tests-execute.md +6 -3
- package/.agent-src/commands/threat-model.md +4 -0
- package/.agent-src/commands/update-form-request-messages.md +4 -0
- package/.agent-src/commands/upstream-contribute.md +4 -0
- package/.agent-src/commands/work.md +161 -0
- package/.agent-src/guidelines/agent-infra/engineering-memory-data-format.md +2 -6
- package/.agent-src/guidelines/agent-infra/layered-settings.md +0 -1
- package/.agent-src/guidelines/agent-infra/memory-access.md +0 -7
- package/.agent-src/guidelines/agent-infra/role-contracts.md +2 -4
- package/.agent-src/guidelines/agent-infra/self-improvement-pipeline.md +0 -1
- package/.agent-src/guidelines/php/patterns/strategy.md +180 -2
- package/.agent-src/personas/README.md +0 -1
- package/.agent-src/rules/artifact-drafting-protocol.md +7 -2
- package/.agent-src/rules/artifact-engagement-recording.md +133 -0
- package/.agent-src/rules/ask-when-uncertain.md +18 -13
- package/.agent-src/rules/augment-portability.md +64 -37
- package/.agent-src/rules/autonomous-execution.md +158 -0
- package/.agent-src/rules/chat-history-cadence.md +109 -0
- package/.agent-src/rules/chat-history-ownership.md +123 -0
- package/.agent-src/rules/chat-history-visibility.md +96 -0
- package/.agent-src/rules/cli-output-handling.md +27 -4
- package/.agent-src/rules/command-suggestion.md +134 -0
- package/.agent-src/rules/commit-policy.md +109 -0
- package/.agent-src/rules/direct-answers.md +114 -0
- package/.agent-src/rules/docs-sync.md +36 -0
- package/.agent-src/rules/downstream-changes.md +10 -9
- package/.agent-src/rules/improve-before-implement.md +9 -6
- package/.agent-src/rules/language-and-tone.md +85 -6
- package/.agent-src/rules/non-destructive-by-default.md +117 -0
- package/.agent-src/rules/package-ci-checks.md +4 -0
- package/.agent-src/rules/preservation-guard.md +20 -0
- package/.agent-src/rules/roadmap-progress-sync.md +159 -27
- package/.agent-src/rules/role-mode-adherence.md +1 -1
- package/.agent-src/rules/scope-control.md +42 -1
- package/.agent-src/rules/size-enforcement.md +2 -3
- package/.agent-src/rules/skill-quality.md +3 -8
- package/.agent-src/rules/ui-audit-before-build.md +106 -0
- package/.agent-src/rules/user-interaction.md +107 -51
- package/.agent-src/scripts/update_roadmap_progress.py +73 -9
- package/.agent-src/skills/blade-ui/SKILL.md +47 -3
- package/.agent-src/skills/command-routing/SKILL.md +32 -0
- package/.agent-src/skills/command-writing/SKILL.md +52 -2
- package/.agent-src/skills/description-assist/SKILL.md +21 -0
- package/.agent-src/skills/estimate-ticket/SKILL.md +0 -1
- package/.agent-src/skills/existing-ui-audit/SKILL.md +202 -0
- package/.agent-src/skills/fe-design/SKILL.md +78 -61
- package/.agent-src/skills/file-editor/SKILL.md +9 -0
- package/.agent-src/skills/finishing-a-development-branch/SKILL.md +4 -0
- package/.agent-src/skills/flux/SKILL.md +31 -4
- package/.agent-src/skills/guideline-writing/SKILL.md +24 -2
- package/.agent-src/skills/learning-to-rule-or-skill/SKILL.md +51 -9
- package/.agent-src/skills/livewire/SKILL.md +49 -4
- package/.agent-src/skills/md-language-check/SKILL.md +103 -0
- package/.agent-src/skills/php-coder/SKILL.md +24 -0
- package/.agent-src/skills/react-shadcn-ui/SKILL.md +121 -0
- package/.agent-src/skills/refine-prompt/SKILL.md +220 -0
- package/.agent-src/skills/refine-ticket/SKILL.md +32 -28
- package/.agent-src/skills/roadmap-management/SKILL.md +24 -11
- package/.agent-src/skills/rule-writing/SKILL.md +23 -1
- package/.agent-src/skills/skill-writing/SKILL.md +3 -5
- package/.agent-src/skills/upstream-contribute/SKILL.md +3 -3
- package/.agent-src/skills/using-git-worktrees/SKILL.md +3 -1
- package/.agent-src/templates/AGENTS.md +24 -6
- package/.agent-src/templates/agent-settings.md +149 -0
- package/.agent-src/templates/roadmaps.md +11 -4
- package/.agent-src/templates/scripts/implement_ticket/__init__.py +63 -26
- package/.agent-src/templates/scripts/implement_ticket/__main__.py +8 -2
- package/.agent-src/templates/scripts/memory_lookup.py +1 -1
- package/.agent-src/templates/scripts/telemetry/__init__.py +42 -0
- package/.agent-src/templates/scripts/telemetry/aggregator.py +154 -0
- package/.agent-src/templates/scripts/telemetry/boundary.py +171 -0
- package/.agent-src/templates/scripts/telemetry/engagement.py +238 -0
- package/.agent-src/templates/scripts/telemetry/report_renderer.py +170 -0
- package/.agent-src/templates/scripts/telemetry/settings.py +112 -0
- package/.agent-src/templates/scripts/telemetry_record.py +166 -0
- package/.agent-src/templates/scripts/telemetry_report.py +161 -0
- package/.agent-src/templates/scripts/telemetry_status.py +142 -0
- package/.agent-src/templates/scripts/work_engine/__init__.py +58 -0
- package/.agent-src/templates/scripts/work_engine/__main__.py +9 -0
- package/.agent-src/templates/scripts/work_engine/cli.py +195 -0
- package/.agent-src/templates/scripts/work_engine/cli_args.py +116 -0
- package/.agent-src/templates/scripts/{implement_ticket → work_engine}/delivery_state.py +10 -3
- package/.agent-src/templates/scripts/work_engine/directives/__init__.py +33 -0
- package/.agent-src/templates/scripts/work_engine/directives/backend/__init__.py +98 -0
- package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/analyze.py +1 -1
- package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/implement.py +3 -3
- package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/memory.py +2 -2
- package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/plan.py +2 -2
- package/.agent-src/templates/scripts/work_engine/directives/backend/refine.py +396 -0
- package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/report.py +37 -5
- package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/test.py +2 -2
- package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/verify.py +2 -2
- package/.agent-src/templates/scripts/work_engine/directives/mixed/__init__.py +116 -0
- package/.agent-src/templates/scripts/work_engine/directives/mixed/contract.py +254 -0
- package/.agent-src/templates/scripts/work_engine/directives/mixed/stitch.py +229 -0
- package/.agent-src/templates/scripts/work_engine/directives/mixed/ui.py +231 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui/__init__.py +113 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui/_passthrough.py +44 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui/apply.py +241 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui/audit.py +414 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui/design.py +335 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui/polish.py +510 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui/review.py +468 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/__init__.py +119 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/_skipped.py +37 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/apply.py +165 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/refine.py +66 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/report.py +62 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/test.py +115 -0
- package/.agent-src/templates/scripts/work_engine/dispatcher.py +331 -0
- package/.agent-src/templates/scripts/work_engine/emitters.py +43 -0
- package/.agent-src/templates/scripts/work_engine/errors.py +19 -0
- package/.agent-src/templates/scripts/work_engine/hook_bootstrap.py +76 -0
- package/.agent-src/templates/scripts/work_engine/hooks/__init__.py +54 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/__init__.py +32 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/_chat_history_base.py +103 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_append.py +44 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_halt_append.py +42 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_heartbeat.py +50 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_turn_check.py +49 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/directive_set_guard.py +53 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/halt_surface_audit.py +50 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/state_shape_validation.py +52 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/trace.py +84 -0
- package/.agent-src/templates/scripts/work_engine/hooks/context.py +66 -0
- package/.agent-src/templates/scripts/work_engine/hooks/events.py +44 -0
- package/.agent-src/templates/scripts/work_engine/hooks/exceptions.py +79 -0
- package/.agent-src/templates/scripts/work_engine/hooks/registry.py +60 -0
- package/.agent-src/templates/scripts/work_engine/hooks/runner.py +73 -0
- package/.agent-src/templates/scripts/work_engine/hooks/settings.py +141 -0
- package/.agent-src/templates/scripts/work_engine/input_builders.py +163 -0
- package/.agent-src/templates/scripts/work_engine/intent/__init__.py +47 -0
- package/.agent-src/templates/scripts/work_engine/intent/classify.py +280 -0
- package/.agent-src/templates/scripts/work_engine/migration/__init__.py +8 -0
- package/.agent-src/templates/scripts/work_engine/migration/v0_to_v1.py +231 -0
- package/.agent-src/templates/scripts/{implement_ticket → work_engine}/persona_policy.py +1 -1
- package/.agent-src/templates/scripts/work_engine/resolvers/__init__.py +22 -0
- package/.agent-src/templates/scripts/work_engine/resolvers/diff.py +106 -0
- package/.agent-src/templates/scripts/work_engine/resolvers/file.py +113 -0
- package/.agent-src/templates/scripts/work_engine/resolvers/prompt.py +90 -0
- package/.agent-src/templates/scripts/work_engine/scoring/__init__.py +14 -0
- package/.agent-src/templates/scripts/work_engine/scoring/confidence.py +300 -0
- package/.agent-src/templates/scripts/work_engine/stack/__init__.py +31 -0
- package/.agent-src/templates/scripts/work_engine/stack/detect.py +187 -0
- package/.agent-src/templates/scripts/work_engine/state.py +641 -0
- package/.agent-src/templates/scripts/work_engine/state_io.py +202 -0
- package/.claude-plugin/marketplace.json +105 -2
- package/AGENTS.md +38 -8
- package/CHANGELOG.md +609 -0
- package/README.md +136 -14
- package/config/agent-settings.template.yml +45 -0
- package/config/gitignore-block.txt +4 -0
- package/docs/MIGRATION.md +122 -0
- package/docs/architecture.md +111 -35
- package/docs/contracts/STABILITY.md +95 -0
- package/docs/contracts/adr-chat-history-split.md +132 -0
- package/docs/contracts/adr-command-suggestion.md +146 -0
- package/docs/contracts/adr-implement-ticket-runtime.md +122 -0
- package/docs/contracts/adr-product-ui-track.md +384 -0
- package/docs/contracts/adr-prompt-driven-execution.md +187 -0
- package/docs/contracts/agent-memory-contract.md +149 -0
- package/docs/contracts/artifact-engagement-flow.md +262 -0
- package/docs/contracts/command-clusters.md +126 -0
- package/docs/contracts/command-suggestion-flow.md +148 -0
- package/docs/contracts/implement-ticket-flow.md +628 -0
- package/docs/contracts/linear-ai-rules-inclusion.md +143 -0
- package/docs/contracts/linear-ai-three-layers.md +131 -0
- package/docs/contracts/rule-interactions.md +107 -0
- package/docs/contracts/rule-interactions.yml +142 -0
- package/docs/contracts/ui-stack-extension.md +236 -0
- package/docs/contracts/ui-track-flow.md +338 -0
- package/docs/development.md +1 -1
- package/docs/getting-started.md +3 -3
- package/docs/installation.md +124 -2
- package/docs/migrations/commands-1.15.0.md +112 -0
- package/docs/showcase.md +204 -0
- package/docs/ui-track-mental-model.md +121 -0
- package/package.json +1 -1
- package/scripts/agent-config +199 -0
- package/scripts/audit_cloud_compatibility.py +288 -0
- package/scripts/build_cloud_bundle.py +458 -0
- package/scripts/build_linear_digest.py +263 -0
- package/scripts/chat_history.py +796 -7
- package/scripts/check_compression.py +139 -0
- package/scripts/check_iron_law_prominence.py +143 -0
- package/scripts/check_md_language.py +159 -0
- package/scripts/check_portability.py +38 -0
- package/scripts/check_public_links.py +185 -0
- package/scripts/check_references.py +1 -0
- package/scripts/check_reply_consistency.py +140 -0
- package/scripts/command_suggester/__init__.py +51 -0
- package/scripts/command_suggester/cooldown.py +132 -0
- package/scripts/command_suggester/loader.py +70 -0
- package/scripts/command_suggester/match.py +180 -0
- package/scripts/command_suggester/rank.py +120 -0
- package/scripts/command_suggester/render.py +86 -0
- package/scripts/command_suggester/sanitize.py +113 -0
- package/scripts/command_suggester/settings.py +125 -0
- package/scripts/command_suggester/types.py +78 -0
- package/scripts/hooks/augment-chat-history.sh +56 -0
- package/scripts/install-hooks.sh +67 -0
- package/scripts/install.py +150 -33
- package/scripts/lint_marketplace.py +27 -0
- package/scripts/lint_no_new_atomic_commands.py +179 -0
- package/scripts/lint_rule_interactions.py +149 -0
- package/scripts/memory_lookup.py +1 -1
- package/scripts/migrate_command_suggestions.py +151 -0
- package/scripts/release.py +297 -64
- package/scripts/schemas/command.schema.json +41 -0
- package/scripts/skill_linter.py +81 -0
- package/scripts/sync_agent_settings.py +42 -12
- package/scripts/update_counts.py +10 -0
- package/templates/consumer-settings/augment-cli-hooks.json +54 -0
- package/templates/consumer-settings/claude-settings.json +55 -1
- package/.agent-src/rules/chat-history.md +0 -171
- package/.agent-src/templates/scripts/implement_ticket/cli.py +0 -171
- package/.agent-src/templates/scripts/implement_ticket/dispatcher.py +0 -134
- package/.agent-src/templates/scripts/implement_ticket/steps/__init__.py +0 -49
- package/.agent-src/templates/scripts/implement_ticket/steps/refine.py +0 -140
|
@@ -0,0 +1,641 @@
|
|
|
1
|
+
"""Schema v1 of the universal-engine work state.
|
|
2
|
+
|
|
3
|
+
The wire format adds five envelope fields on top of the legacy
|
|
4
|
+
``DeliveryState`` shape from ``implement_ticket.delivery_state``:
|
|
5
|
+
|
|
6
|
+
- ``version`` — integer schema version, currently ``1``.
|
|
7
|
+
- ``input.kind`` — typed input variant (only ``"ticket"`` for R1).
|
|
8
|
+
- ``input.data`` — the original payload (was ``state.ticket`` in v0).
|
|
9
|
+
- ``intent`` — coarse intent label (``"backend-coding"`` for R1).
|
|
10
|
+
- ``directive_set`` — name of the directive bundle the dispatcher
|
|
11
|
+
loads. The enum is forward-compatible: ``ui``, ``ui-trivial``, and
|
|
12
|
+
``mixed`` are accepted by the schema even though only ``backend``
|
|
13
|
+
has working directives in R1 (Phase 4 Step 4 — pre-listed to avoid
|
|
14
|
+
a schema bump when R3 V2 lands).
|
|
15
|
+
- ``stack`` — optional ``{frontend, mtime}`` cache populated by
|
|
16
|
+
:mod:`work_engine.stack.detect` (R3 Phase 1). ``None`` while the
|
|
17
|
+
detector has not yet run; the dispatcher fills it on the first UI
|
|
18
|
+
dispatch and re-runs detection when ``mtime`` no longer matches the
|
|
19
|
+
filesystem (manifest edited).
|
|
20
|
+
- ``ui_audit`` — optional inventory written by the
|
|
21
|
+
``existing-ui-audit`` skill (R3 Phase 2). ``None`` while the audit
|
|
22
|
+
has not run; populated dict once the skill returns. ``greenfield``
|
|
23
|
+
flag plus ``greenfield_decision`` carry the user's scaffolding
|
|
24
|
+
pick. The audit gate (:mod:`work_engine.directives.ui.audit`)
|
|
25
|
+
refuses to advance to design/apply while the slot is empty or
|
|
26
|
+
while ``greenfield`` is set without a recorded decision.
|
|
27
|
+
- ``ui_design`` — optional design brief produced by
|
|
28
|
+
:mod:`work_engine.directives.ui.design` (R3 Phase 3 Step 1). Locks
|
|
29
|
+
layout / components / states / microcopy / a11y; ``design_confirmed``
|
|
30
|
+
carries the user's sign-off.
|
|
31
|
+
- ``ui_review`` — optional review-pass output written by
|
|
32
|
+
:mod:`work_engine.directives.ui.review` (R3 Phase 3 Step 4). Carries
|
|
33
|
+
the design-review findings list and a ``review_clean`` flag set when
|
|
34
|
+
no findings remain.
|
|
35
|
+
- ``ui_polish`` — optional polish-pass log written by
|
|
36
|
+
:mod:`work_engine.directives.ui.polish` (R3 Phase 3 Step 5). Tracks
|
|
37
|
+
the round counter (``rounds`` ≤ 2 ceiling) and the per-round
|
|
38
|
+
applied-fix list so a re-entry knows whether the loop has been
|
|
39
|
+
exhausted.
|
|
40
|
+
- ``contract`` — optional backend-contract envelope written by
|
|
41
|
+
:mod:`work_engine.directives.mixed.contract` (R3 Phase 4 Step 1).
|
|
42
|
+
Locks ``data_model`` and ``api_surface`` before any UI work starts;
|
|
43
|
+
``contract_confirmed`` carries the user's sign-off. The mixed UI
|
|
44
|
+
step refuses to advance without a confirmed contract — this is the
|
|
45
|
+
sentinel that prevents UI work from racing ahead of the backend.
|
|
46
|
+
- ``stitch`` — optional integration-verification envelope written by
|
|
47
|
+
:mod:`work_engine.directives.mixed.stitch` (R3 Phase 4 Step 3).
|
|
48
|
+
Carries the end-to-end smoke ``scenarios`` list, an aggregate
|
|
49
|
+
``verdict`` (success / blocked / partial), and the
|
|
50
|
+
``integration_confirmed`` flag the user sets after reviewing the
|
|
51
|
+
integration evidence.
|
|
52
|
+
|
|
53
|
+
All other fields keep their v0 names so the dispatcher can read the
|
|
54
|
+
legacy slice unchanged once Phase 3 wires the steps over.
|
|
55
|
+
|
|
56
|
+
The module exposes:
|
|
57
|
+
|
|
58
|
+
- :data:`SCHEMA_VERSION` — the integer carried in ``version``.
|
|
59
|
+
- :data:`KNOWN_INPUT_KINDS` / :data:`KNOWN_DIRECTIVE_SETS` — the
|
|
60
|
+
whitelists used by validation.
|
|
61
|
+
- :class:`Input`, :class:`WorkState` — typed dataclasses.
|
|
62
|
+
- :func:`from_dict` / :func:`to_dict` — JSON round-trip helpers.
|
|
63
|
+
- :class:`SchemaError` — raised on every validation failure.
|
|
64
|
+
"""
|
|
65
|
+
from __future__ import annotations
|
|
66
|
+
|
|
67
|
+
import json
|
|
68
|
+
from dataclasses import dataclass, field
|
|
69
|
+
from pathlib import Path
|
|
70
|
+
from typing import Any
|
|
71
|
+
|
|
72
|
+
SCHEMA_VERSION = 1
|
|
73
|
+
"""Integer version stored under the ``version`` key on disk."""
|
|
74
|
+
|
|
75
|
+
DEFAULT_INTENT = "backend-coding"
|
|
76
|
+
"""Intent applied when migrating a v0 file or building a fresh state."""
|
|
77
|
+
|
|
78
|
+
DEFAULT_DIRECTIVE_SET = "backend"
|
|
79
|
+
"""Directive set applied when migrating a v0 file or building a fresh state."""
|
|
80
|
+
|
|
81
|
+
KNOWN_INPUT_KINDS: frozenset[str] = frozenset({"ticket", "prompt", "diff", "file"})
|
|
82
|
+
"""Input kinds accepted by the schema.
|
|
83
|
+
|
|
84
|
+
``ticket`` is the R1 kind: pre-structured ``{id, title, acceptance_criteria, …}``
|
|
85
|
+
fed by the ``/implement-ticket`` flow. ``prompt`` is the R2 kind: a free-form
|
|
86
|
+
user prompt wrapped via :mod:`work_engine.resolvers.prompt` into
|
|
87
|
+
``{raw, reconstructed_ac, assumptions}``; the engine refines the raw text
|
|
88
|
+
into actionable AC + a confidence band before plan/apply/test/review run.
|
|
89
|
+
|
|
90
|
+
``diff`` and ``file`` are the R3 Phase 1 UI-improve kinds. ``diff`` carries a
|
|
91
|
+
unified-diff / patch payload (``{raw, reconstructed_ac, assumptions}``) so the
|
|
92
|
+
``directives/ui`` set can take an "improve this screen" PR-style input; ``file``
|
|
93
|
+
carries a path reference to an existing component/page (``{path,
|
|
94
|
+
reconstructed_ac, assumptions}``) for the same surface. Both default-route to
|
|
95
|
+
``ui-improve`` via :func:`work_engine.intent.populate_routing`.
|
|
96
|
+
|
|
97
|
+
Per the schema/capability split documented on
|
|
98
|
+
:data:`work_engine.directives.backend.SUPPORTED_KINDS`, presence here only
|
|
99
|
+
means the *envelope* is accepted on disk — a directive set still has to
|
|
100
|
+
list the kind in its ``SUPPORTED_KINDS`` tuple before the dispatcher will
|
|
101
|
+
route it. R2 widens the envelope; R2 Phase 3 widens backend's capability
|
|
102
|
+
tuple in lockstep with the ``refine-prompt`` skill landing. R3 Phase 1 widens
|
|
103
|
+
the envelope further; ``ui`` capability is wired in Phase 3 of the UI track.
|
|
104
|
+
|
|
105
|
+
Other kinds are rejected so unknown values surface as errors instead of
|
|
106
|
+
silently falling through to a default branch."""
|
|
107
|
+
|
|
108
|
+
KNOWN_DIRECTIVE_SETS: frozenset[str] = frozenset(
|
|
109
|
+
{"backend", "ui", "ui-trivial", "mixed"},
|
|
110
|
+
)
|
|
111
|
+
"""Directive sets recognised by the schema.
|
|
112
|
+
|
|
113
|
+
Per the roadmap (Phase 4 Step 4), ``ui``, ``ui-trivial``, and ``mixed``
|
|
114
|
+
are intentionally pre-listed so a future R3 V2 release does not need a
|
|
115
|
+
schema bump. Only ``backend`` has working directives in R1; the others
|
|
116
|
+
raise ``NotImplementedError`` at dispatch time."""
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class SchemaError(ValueError):
|
|
120
|
+
"""Raised when a state payload violates the v1 contract."""
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass
|
|
124
|
+
class Input:
|
|
125
|
+
"""Typed envelope for the user-supplied work item.
|
|
126
|
+
|
|
127
|
+
``kind`` is one of :data:`KNOWN_INPUT_KINDS`; ``data`` is the raw
|
|
128
|
+
payload. The legacy ``state.ticket`` dict lands here as
|
|
129
|
+
``Input(kind="ticket", data=<dict>)`` after migration.
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
kind: str
|
|
133
|
+
data: dict[str, Any] = field(default_factory=dict)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@dataclass
|
|
137
|
+
class WorkState:
|
|
138
|
+
"""Schema v1 of the persisted work state.
|
|
139
|
+
|
|
140
|
+
Field order mirrors the on-disk JSON: envelope (``version``,
|
|
141
|
+
``input``, ``intent``, ``directive_set``, ``stack``) first, then
|
|
142
|
+
the legacy ``DeliveryState`` slice (``persona`` … ``report``) so a
|
|
143
|
+
diff between a v1 file and its v0 ancestor stays readable.
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
input: Input
|
|
147
|
+
intent: str = DEFAULT_INTENT
|
|
148
|
+
directive_set: str = DEFAULT_DIRECTIVE_SET
|
|
149
|
+
stack: dict[str, Any] | None = None
|
|
150
|
+
ui_audit: dict[str, Any] | None = None
|
|
151
|
+
ui_design: dict[str, Any] | None = None
|
|
152
|
+
ui_review: dict[str, Any] | None = None
|
|
153
|
+
ui_polish: dict[str, Any] | None = None
|
|
154
|
+
contract: dict[str, Any] | None = None
|
|
155
|
+
stitch: dict[str, Any] | None = None
|
|
156
|
+
version: int = SCHEMA_VERSION
|
|
157
|
+
persona: str = "senior-engineer"
|
|
158
|
+
memory: list[dict[str, Any]] = field(default_factory=list)
|
|
159
|
+
plan: Any = None
|
|
160
|
+
changes: list[dict[str, Any]] = field(default_factory=list)
|
|
161
|
+
tests: Any = None
|
|
162
|
+
verify: Any = None
|
|
163
|
+
outcomes: dict[str, str] = field(default_factory=dict)
|
|
164
|
+
questions: list[str] = field(default_factory=list)
|
|
165
|
+
report: str = ""
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def to_dict(state: WorkState) -> dict[str, Any]:
|
|
169
|
+
"""Serialise ``state`` to the canonical v1 JSON shape.
|
|
170
|
+
|
|
171
|
+
Field order is fixed: ``version`` → ``input`` → ``intent`` →
|
|
172
|
+
``directive_set`` → legacy slice. Stable order keeps state
|
|
173
|
+
snapshots diff-friendly across re-runs and across the freeze-guard
|
|
174
|
+
replay. Validation runs before serialisation so an in-memory
|
|
175
|
+
object that was mutated past the schema cannot reach disk.
|
|
176
|
+
"""
|
|
177
|
+
_validate_kind(state.input.kind)
|
|
178
|
+
_validate_directive_set(state.directive_set)
|
|
179
|
+
if state.version != SCHEMA_VERSION:
|
|
180
|
+
raise SchemaError(
|
|
181
|
+
f"version must be {SCHEMA_VERSION}; got {state.version!r}",
|
|
182
|
+
)
|
|
183
|
+
_validate_stack(state.stack)
|
|
184
|
+
_validate_ui_audit(state.ui_audit)
|
|
185
|
+
_validate_ui_design(state.ui_design)
|
|
186
|
+
_validate_ui_review(state.ui_review)
|
|
187
|
+
_validate_ui_polish(state.ui_polish)
|
|
188
|
+
_validate_contract(state.contract)
|
|
189
|
+
_validate_stitch(state.stitch)
|
|
190
|
+
return {
|
|
191
|
+
"version": state.version,
|
|
192
|
+
"input": {"kind": state.input.kind, "data": state.input.data},
|
|
193
|
+
"intent": state.intent,
|
|
194
|
+
"directive_set": state.directive_set,
|
|
195
|
+
"stack": state.stack,
|
|
196
|
+
"ui_audit": state.ui_audit,
|
|
197
|
+
"ui_design": state.ui_design,
|
|
198
|
+
"ui_review": state.ui_review,
|
|
199
|
+
"ui_polish": state.ui_polish,
|
|
200
|
+
"contract": state.contract,
|
|
201
|
+
"stitch": state.stitch,
|
|
202
|
+
"persona": state.persona,
|
|
203
|
+
"memory": state.memory,
|
|
204
|
+
"plan": state.plan,
|
|
205
|
+
"changes": state.changes,
|
|
206
|
+
"tests": state.tests,
|
|
207
|
+
"verify": state.verify,
|
|
208
|
+
"outcomes": state.outcomes,
|
|
209
|
+
"questions": state.questions,
|
|
210
|
+
"report": state.report,
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def from_dict(payload: Any) -> WorkState:
|
|
215
|
+
"""Build a :class:`WorkState` from a parsed JSON payload.
|
|
216
|
+
|
|
217
|
+
Validates the envelope (``version``, ``input.kind``,
|
|
218
|
+
``directive_set``) before instantiating the dataclass. Unknown
|
|
219
|
+
top-level keys are tolerated and dropped — the schema is additive,
|
|
220
|
+
not strict-rejecting, so a future field rolled out by a newer
|
|
221
|
+
engine version does not crash an older reader.
|
|
222
|
+
"""
|
|
223
|
+
if not isinstance(payload, dict):
|
|
224
|
+
raise SchemaError(
|
|
225
|
+
f"state payload must be a JSON object; got {type(payload).__name__}",
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
version = payload.get("version")
|
|
229
|
+
if version != SCHEMA_VERSION:
|
|
230
|
+
raise SchemaError(
|
|
231
|
+
f"version must be {SCHEMA_VERSION}; got {version!r}. "
|
|
232
|
+
"Run the v0→v1 migration before loading legacy files.",
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
raw_input = payload.get("input")
|
|
236
|
+
if not isinstance(raw_input, dict):
|
|
237
|
+
raise SchemaError(
|
|
238
|
+
"state.input must be a JSON object with 'kind' and 'data' keys",
|
|
239
|
+
)
|
|
240
|
+
kind = raw_input.get("kind")
|
|
241
|
+
_validate_kind(kind)
|
|
242
|
+
data = raw_input.get("data", {})
|
|
243
|
+
if not isinstance(data, dict):
|
|
244
|
+
raise SchemaError(
|
|
245
|
+
f"state.input.data must be a JSON object; got {type(data).__name__}",
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
directive_set = payload.get("directive_set", DEFAULT_DIRECTIVE_SET)
|
|
249
|
+
_validate_directive_set(directive_set)
|
|
250
|
+
|
|
251
|
+
stack = payload.get("stack")
|
|
252
|
+
_validate_stack(stack)
|
|
253
|
+
|
|
254
|
+
ui_audit = payload.get("ui_audit")
|
|
255
|
+
_validate_ui_audit(ui_audit)
|
|
256
|
+
|
|
257
|
+
ui_design = payload.get("ui_design")
|
|
258
|
+
_validate_ui_design(ui_design)
|
|
259
|
+
|
|
260
|
+
ui_review = payload.get("ui_review")
|
|
261
|
+
_validate_ui_review(ui_review)
|
|
262
|
+
|
|
263
|
+
ui_polish = payload.get("ui_polish")
|
|
264
|
+
_validate_ui_polish(ui_polish)
|
|
265
|
+
|
|
266
|
+
contract = payload.get("contract")
|
|
267
|
+
_validate_contract(contract)
|
|
268
|
+
|
|
269
|
+
stitch = payload.get("stitch")
|
|
270
|
+
_validate_stitch(stitch)
|
|
271
|
+
|
|
272
|
+
return WorkState(
|
|
273
|
+
input=Input(kind=kind, data=data),
|
|
274
|
+
intent=payload.get("intent", DEFAULT_INTENT),
|
|
275
|
+
directive_set=directive_set,
|
|
276
|
+
stack=dict(stack) if isinstance(stack, dict) else None,
|
|
277
|
+
ui_audit=dict(ui_audit) if isinstance(ui_audit, dict) else None,
|
|
278
|
+
ui_design=dict(ui_design) if isinstance(ui_design, dict) else None,
|
|
279
|
+
ui_review=dict(ui_review) if isinstance(ui_review, dict) else None,
|
|
280
|
+
ui_polish=dict(ui_polish) if isinstance(ui_polish, dict) else None,
|
|
281
|
+
contract=dict(contract) if isinstance(contract, dict) else None,
|
|
282
|
+
stitch=dict(stitch) if isinstance(stitch, dict) else None,
|
|
283
|
+
version=version,
|
|
284
|
+
persona=payload.get("persona", "senior-engineer"),
|
|
285
|
+
memory=list(payload.get("memory", [])),
|
|
286
|
+
plan=payload.get("plan"),
|
|
287
|
+
changes=list(payload.get("changes", [])),
|
|
288
|
+
tests=payload.get("tests"),
|
|
289
|
+
verify=payload.get("verify"),
|
|
290
|
+
outcomes=dict(payload.get("outcomes", {})),
|
|
291
|
+
questions=list(payload.get("questions", [])),
|
|
292
|
+
report=payload.get("report", ""),
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def load(path: Path) -> WorkState:
|
|
297
|
+
"""Read a v1 state file from disk and return a :class:`WorkState`."""
|
|
298
|
+
raw = path.read_text(encoding="utf-8")
|
|
299
|
+
try:
|
|
300
|
+
payload = json.loads(raw)
|
|
301
|
+
except json.JSONDecodeError as exc:
|
|
302
|
+
raise SchemaError(f"invalid JSON in {path}: {exc}") from exc
|
|
303
|
+
return from_dict(payload)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def dump(state: WorkState, path: Path) -> None:
|
|
307
|
+
"""Write ``state`` to ``path`` as pretty JSON, terminating newline included."""
|
|
308
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
309
|
+
path.write_text(
|
|
310
|
+
json.dumps(to_dict(state), indent=2, ensure_ascii=False) + "\n",
|
|
311
|
+
encoding="utf-8",
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _validate_kind(kind: Any) -> None:
|
|
316
|
+
if not isinstance(kind, str):
|
|
317
|
+
raise SchemaError(
|
|
318
|
+
f"state.input.kind must be a string; got {type(kind).__name__}",
|
|
319
|
+
)
|
|
320
|
+
if kind not in KNOWN_INPUT_KINDS:
|
|
321
|
+
raise SchemaError(
|
|
322
|
+
f"unknown input.kind {kind!r}; "
|
|
323
|
+
f"expected one of {sorted(KNOWN_INPUT_KINDS)}",
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _validate_directive_set(name: Any) -> None:
|
|
328
|
+
if not isinstance(name, str):
|
|
329
|
+
raise SchemaError(
|
|
330
|
+
f"state.directive_set must be a string; got {type(name).__name__}",
|
|
331
|
+
)
|
|
332
|
+
if name not in KNOWN_DIRECTIVE_SETS:
|
|
333
|
+
raise SchemaError(
|
|
334
|
+
f"unknown directive_set {name!r}; "
|
|
335
|
+
f"expected one of {sorted(KNOWN_DIRECTIVE_SETS)}",
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _validate_stack(stack: Any) -> None:
|
|
340
|
+
"""Reject malformed stack envelopes; tolerate ``None`` (not yet detected).
|
|
341
|
+
|
|
342
|
+
The detector populates ``state.stack`` lazily — the first dispatch
|
|
343
|
+
of a new state file may run without it set, then the dispatcher
|
|
344
|
+
fills it in before any UI handler reads it. We only validate the
|
|
345
|
+
shape when present so the absence-of-detection case stays a normal
|
|
346
|
+
code path, not an error.
|
|
347
|
+
"""
|
|
348
|
+
if stack is None:
|
|
349
|
+
return
|
|
350
|
+
if not isinstance(stack, dict):
|
|
351
|
+
raise SchemaError(
|
|
352
|
+
f"state.stack must be a JSON object or null; "
|
|
353
|
+
f"got {type(stack).__name__}",
|
|
354
|
+
)
|
|
355
|
+
frontend = stack.get("frontend")
|
|
356
|
+
if not isinstance(frontend, str) or not frontend:
|
|
357
|
+
raise SchemaError(
|
|
358
|
+
"state.stack.frontend must be a non-empty string",
|
|
359
|
+
)
|
|
360
|
+
mtime = stack.get("mtime", 0.0)
|
|
361
|
+
if not isinstance(mtime, (int, float)):
|
|
362
|
+
raise SchemaError(
|
|
363
|
+
f"state.stack.mtime must be a number; got {type(mtime).__name__}",
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _validate_ui_audit(ui_audit: Any) -> None:
|
|
368
|
+
"""Reject malformed ``ui_audit`` envelopes; tolerate ``None`` and ``{}``.
|
|
369
|
+
|
|
370
|
+
``None`` means the audit has not run yet — the dispatcher's audit
|
|
371
|
+
gate (``directives.ui.audit``) will emit the agent-directive that
|
|
372
|
+
populates it. An empty dict is the in-progress shape after the
|
|
373
|
+
skill returns but before findings land; the gate treats it the
|
|
374
|
+
same as ``None``. Once populated, ``greenfield`` (when present)
|
|
375
|
+
must be a bool, and ``greenfield_decision`` (when present) must
|
|
376
|
+
be one of the three documented choices. Other keys (``components``,
|
|
377
|
+
``patterns``, …) are validated by the audit handler against the
|
|
378
|
+
skill contract — the schema only enforces shape, not content.
|
|
379
|
+
"""
|
|
380
|
+
if ui_audit is None:
|
|
381
|
+
return
|
|
382
|
+
if not isinstance(ui_audit, dict):
|
|
383
|
+
raise SchemaError(
|
|
384
|
+
f"state.ui_audit must be a JSON object or null; "
|
|
385
|
+
f"got {type(ui_audit).__name__}",
|
|
386
|
+
)
|
|
387
|
+
if "greenfield" in ui_audit and not isinstance(
|
|
388
|
+
ui_audit["greenfield"], bool,
|
|
389
|
+
):
|
|
390
|
+
raise SchemaError(
|
|
391
|
+
"state.ui_audit.greenfield must be a boolean when present",
|
|
392
|
+
)
|
|
393
|
+
decision = ui_audit.get("greenfield_decision")
|
|
394
|
+
if decision is not None and decision not in {
|
|
395
|
+
"scaffold",
|
|
396
|
+
"bare",
|
|
397
|
+
"external_reference",
|
|
398
|
+
}:
|
|
399
|
+
raise SchemaError(
|
|
400
|
+
f"state.ui_audit.greenfield_decision must be one of "
|
|
401
|
+
f"'scaffold', 'bare', 'external_reference', or null; "
|
|
402
|
+
f"got {decision!r}",
|
|
403
|
+
)
|
|
404
|
+
if "a11y_baseline" in ui_audit and not isinstance(
|
|
405
|
+
ui_audit["a11y_baseline"], list,
|
|
406
|
+
):
|
|
407
|
+
raise SchemaError(
|
|
408
|
+
"state.ui_audit.a11y_baseline must be a list when present",
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _validate_ui_design(ui_design: Any) -> None:
|
|
413
|
+
"""Reject malformed ``ui_design`` envelopes; tolerate ``None`` and ``{}``.
|
|
414
|
+
|
|
415
|
+
``None`` means the design step has not produced a brief yet — the
|
|
416
|
+
dispatcher's design gate (``directives.ui.design``) emits the
|
|
417
|
+
agent-directive that populates it. An empty dict is the in-progress
|
|
418
|
+
shape after the skill returns but before the brief lands; the gate
|
|
419
|
+
treats it the same as ``None``. Once populated, ``design_confirmed``
|
|
420
|
+
(when present) must be a bool. Other keys (``layout``, ``components``,
|
|
421
|
+
``states``, ``microcopy``, ``a11y``, ``reused_from_audit``) are
|
|
422
|
+
validated by the design handler against the skill contract — the
|
|
423
|
+
schema only enforces shape, not content.
|
|
424
|
+
"""
|
|
425
|
+
if ui_design is None:
|
|
426
|
+
return
|
|
427
|
+
if not isinstance(ui_design, dict):
|
|
428
|
+
raise SchemaError(
|
|
429
|
+
f"state.ui_design must be a JSON object or null; "
|
|
430
|
+
f"got {type(ui_design).__name__}",
|
|
431
|
+
)
|
|
432
|
+
if "design_confirmed" in ui_design and not isinstance(
|
|
433
|
+
ui_design["design_confirmed"], bool,
|
|
434
|
+
):
|
|
435
|
+
raise SchemaError(
|
|
436
|
+
"state.ui_design.design_confirmed must be a boolean when present",
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _validate_ui_review(ui_review: Any) -> None:
|
|
441
|
+
"""Reject malformed ``ui_review`` envelopes; tolerate ``None`` and ``{}``.
|
|
442
|
+
|
|
443
|
+
``None`` means the review pass has not run yet — the dispatcher's
|
|
444
|
+
review gate (``directives.ui.review``) emits the agent-directive
|
|
445
|
+
that populates it. An empty dict is the in-progress shape after
|
|
446
|
+
the skill returns but before findings land. Once populated,
|
|
447
|
+
``findings`` (when present) must be a list and ``review_clean``
|
|
448
|
+
(when present) must be a bool. Field content (severity labels,
|
|
449
|
+
fix suggestions) is validated by the review handler; the schema
|
|
450
|
+
enforces only shape.
|
|
451
|
+
"""
|
|
452
|
+
if ui_review is None:
|
|
453
|
+
return
|
|
454
|
+
if not isinstance(ui_review, dict):
|
|
455
|
+
raise SchemaError(
|
|
456
|
+
f"state.ui_review must be a JSON object or null; "
|
|
457
|
+
f"got {type(ui_review).__name__}",
|
|
458
|
+
)
|
|
459
|
+
if "findings" in ui_review and not isinstance(ui_review["findings"], list):
|
|
460
|
+
raise SchemaError(
|
|
461
|
+
"state.ui_review.findings must be a list when present",
|
|
462
|
+
)
|
|
463
|
+
if "review_clean" in ui_review and not isinstance(
|
|
464
|
+
ui_review["review_clean"], bool,
|
|
465
|
+
):
|
|
466
|
+
raise SchemaError(
|
|
467
|
+
"state.ui_review.review_clean must be a boolean when present",
|
|
468
|
+
)
|
|
469
|
+
a11y = ui_review.get("a11y")
|
|
470
|
+
if a11y is not None:
|
|
471
|
+
if not isinstance(a11y, dict):
|
|
472
|
+
raise SchemaError(
|
|
473
|
+
"state.ui_review.a11y must be a JSON object or null when present",
|
|
474
|
+
)
|
|
475
|
+
if "violations" in a11y and not isinstance(a11y["violations"], list):
|
|
476
|
+
raise SchemaError(
|
|
477
|
+
"state.ui_review.a11y.violations must be a list when present",
|
|
478
|
+
)
|
|
479
|
+
floor = a11y.get("severity_floor")
|
|
480
|
+
if floor is not None and floor not in {
|
|
481
|
+
"minor",
|
|
482
|
+
"moderate",
|
|
483
|
+
"serious",
|
|
484
|
+
"critical",
|
|
485
|
+
}:
|
|
486
|
+
raise SchemaError(
|
|
487
|
+
f"state.ui_review.a11y.severity_floor must be one of "
|
|
488
|
+
f"'minor', 'moderate', 'serious', 'critical', or null; "
|
|
489
|
+
f"got {floor!r}",
|
|
490
|
+
)
|
|
491
|
+
if "accepted_violations" in a11y and not isinstance(
|
|
492
|
+
a11y["accepted_violations"], list,
|
|
493
|
+
):
|
|
494
|
+
raise SchemaError(
|
|
495
|
+
"state.ui_review.a11y.accepted_violations must be a list when present",
|
|
496
|
+
)
|
|
497
|
+
preview = ui_review.get("preview")
|
|
498
|
+
if preview is not None:
|
|
499
|
+
if not isinstance(preview, dict):
|
|
500
|
+
raise SchemaError(
|
|
501
|
+
"state.ui_review.preview must be a JSON object or null when present",
|
|
502
|
+
)
|
|
503
|
+
if "render_ok" in preview and not isinstance(preview["render_ok"], bool):
|
|
504
|
+
raise SchemaError(
|
|
505
|
+
"state.ui_review.preview.render_ok must be a boolean when present",
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def _validate_ui_polish(ui_polish: Any) -> None:
|
|
510
|
+
"""Reject malformed ``ui_polish`` envelopes; tolerate ``None`` and ``{}``.
|
|
511
|
+
|
|
512
|
+
``None`` means the polish loop has not entered yet. Once
|
|
513
|
+
populated, ``rounds`` (when present) must be an int in ``[0, 2]``
|
|
514
|
+
by default — the polish-loop ceiling defined in
|
|
515
|
+
``agents/roadmaps/road-to-product-ui-track.md`` Phase 3 Step 5.
|
|
516
|
+
R4 Phase 2 widens the upper bound to ``3`` when
|
|
517
|
+
``extension_used`` is ``True`` (one-shot a11y extension halt).
|
|
518
|
+
``applied`` (when present) must be a list. The polish handler
|
|
519
|
+
enforces ceiling semantics; the schema enforces only shape.
|
|
520
|
+
"""
|
|
521
|
+
if ui_polish is None:
|
|
522
|
+
return
|
|
523
|
+
if not isinstance(ui_polish, dict):
|
|
524
|
+
raise SchemaError(
|
|
525
|
+
f"state.ui_polish must be a JSON object or null; "
|
|
526
|
+
f"got {type(ui_polish).__name__}",
|
|
527
|
+
)
|
|
528
|
+
if "extension_used" in ui_polish and not isinstance(
|
|
529
|
+
ui_polish["extension_used"], bool,
|
|
530
|
+
):
|
|
531
|
+
raise SchemaError(
|
|
532
|
+
"state.ui_polish.extension_used must be a boolean when present",
|
|
533
|
+
)
|
|
534
|
+
extension_used = bool(ui_polish.get("extension_used", False))
|
|
535
|
+
if "rounds" in ui_polish:
|
|
536
|
+
rounds = ui_polish["rounds"]
|
|
537
|
+
if not isinstance(rounds, int) or isinstance(rounds, bool):
|
|
538
|
+
raise SchemaError(
|
|
539
|
+
f"state.ui_polish.rounds must be an integer; got {type(rounds).__name__}",
|
|
540
|
+
)
|
|
541
|
+
max_rounds = 3 if extension_used else 2
|
|
542
|
+
if rounds < 0 or rounds > max_rounds:
|
|
543
|
+
raise SchemaError(
|
|
544
|
+
f"state.ui_polish.rounds must be in [0, {max_rounds}]; "
|
|
545
|
+
f"got {rounds} (extension_used={extension_used})",
|
|
546
|
+
)
|
|
547
|
+
if "applied" in ui_polish and not isinstance(ui_polish["applied"], list):
|
|
548
|
+
raise SchemaError(
|
|
549
|
+
"state.ui_polish.applied must be a list when present",
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def _validate_contract(contract: Any) -> None:
|
|
554
|
+
"""Reject malformed ``contract`` envelopes; tolerate ``None`` and ``{}``.
|
|
555
|
+
|
|
556
|
+
``None`` means the contract step has not run yet — the mixed
|
|
557
|
+
``contract`` directive (R3 Phase 4 Step 1) emits the
|
|
558
|
+
agent-directive that populates it. Once populated,
|
|
559
|
+
``data_model`` and ``api_surface`` (when present) must be lists,
|
|
560
|
+
and ``contract_confirmed`` (when present) must be a bool. Field
|
|
561
|
+
content (entity names, endpoint shapes) is validated by the
|
|
562
|
+
contract handler; the schema enforces only shape so the mixed UI
|
|
563
|
+
step's sentinel check (``contract_confirmed is True``) stays a
|
|
564
|
+
simple equality test.
|
|
565
|
+
"""
|
|
566
|
+
if contract is None:
|
|
567
|
+
return
|
|
568
|
+
if not isinstance(contract, dict):
|
|
569
|
+
raise SchemaError(
|
|
570
|
+
f"state.contract must be a JSON object or null; "
|
|
571
|
+
f"got {type(contract).__name__}",
|
|
572
|
+
)
|
|
573
|
+
if "data_model" in contract and not isinstance(contract["data_model"], list):
|
|
574
|
+
raise SchemaError(
|
|
575
|
+
"state.contract.data_model must be a list when present",
|
|
576
|
+
)
|
|
577
|
+
if "api_surface" in contract and not isinstance(contract["api_surface"], list):
|
|
578
|
+
raise SchemaError(
|
|
579
|
+
"state.contract.api_surface must be a list when present",
|
|
580
|
+
)
|
|
581
|
+
if "contract_confirmed" in contract and not isinstance(
|
|
582
|
+
contract["contract_confirmed"], bool
|
|
583
|
+
):
|
|
584
|
+
raise SchemaError(
|
|
585
|
+
"state.contract.contract_confirmed must be a bool when present",
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def _validate_stitch(stitch: Any) -> None:
|
|
590
|
+
"""Reject malformed ``stitch`` envelopes; tolerate ``None`` and ``{}``.
|
|
591
|
+
|
|
592
|
+
``None`` means the integration-verification step has not run yet
|
|
593
|
+
— the mixed ``stitch`` directive (R3 Phase 4 Step 3) emits the
|
|
594
|
+
agent-directive that populates it. Once populated, ``scenarios``
|
|
595
|
+
(when present) must be a list of integration smoke cases,
|
|
596
|
+
``verdict`` (when present) must be one of
|
|
597
|
+
``{"success", "blocked", "partial"}``, and
|
|
598
|
+
``integration_confirmed`` (when present) must be a bool. The
|
|
599
|
+
stitch handler enforces verdict semantics; the schema enforces
|
|
600
|
+
only shape.
|
|
601
|
+
"""
|
|
602
|
+
if stitch is None:
|
|
603
|
+
return
|
|
604
|
+
if not isinstance(stitch, dict):
|
|
605
|
+
raise SchemaError(
|
|
606
|
+
f"state.stitch must be a JSON object or null; "
|
|
607
|
+
f"got {type(stitch).__name__}",
|
|
608
|
+
)
|
|
609
|
+
if "scenarios" in stitch and not isinstance(stitch["scenarios"], list):
|
|
610
|
+
raise SchemaError(
|
|
611
|
+
"state.stitch.scenarios must be a list when present",
|
|
612
|
+
)
|
|
613
|
+
if "verdict" in stitch:
|
|
614
|
+
verdict = stitch["verdict"]
|
|
615
|
+
if verdict not in {"success", "blocked", "partial"}:
|
|
616
|
+
raise SchemaError(
|
|
617
|
+
f"state.stitch.verdict must be one of success/blocked/partial; "
|
|
618
|
+
f"got {verdict!r}",
|
|
619
|
+
)
|
|
620
|
+
if "integration_confirmed" in stitch and not isinstance(
|
|
621
|
+
stitch["integration_confirmed"], bool
|
|
622
|
+
):
|
|
623
|
+
raise SchemaError(
|
|
624
|
+
"state.stitch.integration_confirmed must be a bool when present",
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
__all__ = [
|
|
629
|
+
"DEFAULT_DIRECTIVE_SET",
|
|
630
|
+
"DEFAULT_INTENT",
|
|
631
|
+
"Input",
|
|
632
|
+
"KNOWN_DIRECTIVE_SETS",
|
|
633
|
+
"KNOWN_INPUT_KINDS",
|
|
634
|
+
"SCHEMA_VERSION",
|
|
635
|
+
"SchemaError",
|
|
636
|
+
"WorkState",
|
|
637
|
+
"dump",
|
|
638
|
+
"from_dict",
|
|
639
|
+
"load",
|
|
640
|
+
"to_dict",
|
|
641
|
+
]
|