Compare commits

...
Author SHA1 Message Date
sorokin.oleg.edu c06a953b97 Sync agent instructions from main 2026-09-13 11:39:13 +03:00
sorokin.oleg.edu 7dcfb3cb10 WIP: save unverified diagram Delete candidate at user pause 2026-09-12 04:53:12 -07:00
sorokin.oleg.edu 5a7018117d Archive combined Windows allocation and repair baseline with review limits 2026-09-12 04:27:56 -07:00
sorokin.oleg.edu 85173a3c95 Refine allocation feedback to preserve collapsed-target reachability 2026-09-12 04:26:05 -07:00
sorokin.oleg.edu d9b3f0dc91 Format saved allocation readability regressions 2026-09-12 04:18:42 -07:00
sorokin.oleg.edu 94b5eecb1c Merge remote-tracking branch 'refs/remotes/gitea/release1-956atf' into codex/behavior-authoring-20260910 2026-09-12 04:18:04 -07:00
sorokin.oleg.edu 6b9cc77ece Save unverified allocation and repair work at user-requested pause 2026-09-12 04:01:44 -07:00
sorokin.oleg.edu bc8bc4f5a1 Archive exact a31b40d Windows and OA native acceptance evidence 2026-09-12 03:54:36 -07:00
sorokin.oleg.edu f52d81c1b9 Integrate allocation drag candidate with guarded input and atomic history 2026-09-12 03:47:45 -07:00
sorokin.oleg.edu 97c91cc06c Sync agent instructions from main 2026-09-12 13:43:41 +03:00
sorokin.oleg.edu 0bd7886608 Merge branch 'codex/behavior-authoring-20260910' of http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio into codex/behavior-authoring-20260910 2026-09-12 03:43:17 -07:00
sorokin.oleg.edu 563772dc62 Sync agent instructions from main 2026-09-12 13:43:11 +03:00
sorokin.oleg.edu eefdedaf47 Merge 2026-09-12 03:41:44 -07:00
sorokin.oleg.edu bc88acd932 Sync agent instructions from main 2026-09-12 13:40:24 +03:00
sorokin.oleg.edu 17d56c06f7 Sync agent instructions from main 2026-09-12 13:39:54 +03:00
sorokin.oleg.edu bcb7057ab2 Sync agent instructions from main 2026-09-12 13:16:01 +03:00
sorokin.oleg.edu 36eff7225a Sync agent instructions from main 2026-09-12 13:15:23 +03:00
sorokin.oleg.edu dd38c46a4b Record user pause and exact Windows acceptance handoff 2026-09-11 12:28:15 -07:00
sorokin.oleg.edu a31b40d0f8 Retire obsolete behavior canvas scroll and tool paths 2026-09-11 11:28:08 -07:00
sorokin.oleg.edu 88c17a448f Record verified canvas union gates and allocation drag contract 2026-09-11 11:24:41 -07:00
sorokin.oleg.edu abffa4b3f0 Simplify canvas control focus guard for strict lint 2026-09-11 11:08:37 -07:00
sorokin.oleg.edu 82fd0acf64 Update behavior guide for canvas palette and camera controls 2026-09-11 11:03:39 -07:00
sorokin.oleg.edu ff0175488a Keep concrete canvas App access within the desktop crate 2026-09-11 10:55:15 -07:00
sorokin.oleg.edu 06ec962129 Adapt mounted canvas tests to installed renderer and workflow APIs 2026-09-11 10:48:35 -07:00
sorokin.oleg.edu 6969c825a8 Integrate shared behavior canvas with current form focus and recovery 2026-09-11 10:45:44 -07:00
sorokin.oleg.edu 638d06c4e3 Merge remote-tracking branch 'gitea/release1-956atf' into codex/behavior-authoring-20260910 2026-09-11 10:30:29 -07:00
sorokin.oleg.edu 41fa5d530c Record Windows scenario and codec gates with shared handoff 2026-09-11 10:29:03 -07:00
sorokin.oleg.edu 828c50c491 Merge updated user goal and shared working agreement 2026-09-11 10:05:23 -07:00
sorokin.oleg.edu f8a6a5264a Sync agent instructions from main 2026-09-11 19:52:09 +03:00
sorokin.oleg.edu b4c8fd7102 Sync agent instructions from main 2026-09-11 19:51:34 +03:00
Codex e7491fb00a test(desktop): preserve final 720-test and strict lint acceptance 2026-09-11 09:25:52 -07:00
Codex bef1ea33e4 docs(harness): record Windows gates and defer Linux execution 2026-09-11 09:12:30 -07:00
Codex 3c9e6eeb64 fix(review): keep workflow messages compact 2026-09-11 09:08:46 -07:00
Codex 80f2588214 fix(forms): reset replacement sessions and preserve visible scroll 2026-09-11 08:33:06 -07:00
Codex 8895dd2e0f fix(scenario): restore focus only to the current parent form 2026-09-11 08:07:41 -07:00
Codex 8d55d2d2eb Save complete restart handoff for user-requested reboot 2026-09-11 06:47:37 -07:00
Codex 03d20ec296 Preserve exact numeric gate logs and bounded decoder review 2026-09-11 06:41:33 -07:00
sorokin.oleg.edu c0e66f94e3 Stage explicit prerequisite sources inside behavior creation forms 2026-09-11 06:26:16 -07:00
Codex 7f081ad457 Integrate scoped reference chooser and scenario form focus and reveal 2026-09-11 06:24:20 -07:00
Codex c7ed894900 Merge shared instruction checkpoint preserving exact authoritative goal 2026-09-11 06:14:11 -07:00
Codex 98427b9620 Adopt shared Gitea and harness maintenance instructions 2026-09-11 06:14:05 -07:00
sorokin.oleg.edu cab01c3d7f Sync agent instructions from main 2026-09-11 15:55:34 +03:00
sorokin.oleg.edu d8bdbd9e48 Sync agent instructions from main 2026-09-11 15:55:01 +03:00
sorokin.oleg.edu 1e9f0c2a97 Sync agent instructions from main 2026-09-11 14:34:18 +03:00
386 changed files with 130617 additions and 3640 deletions
+20
View File
@@ -35,6 +35,13 @@ harness/experiments/library-compatibility-20260911/** -text whitespace=cr-at-eol
harness/experiments/refined-contracts-review-20260911/** -text whitespace=cr-at-eol
harness/behavior-creation-contract.rs -text whitespace=cr-at-eol
harness/experiments/source-cli-independent-20260911/** -text whitespace=cr-at-eol
harness/experiments/authoring-prerelease-20260911/** -text whitespace=cr-at-eol
harness/experiments/cli-packaging-20260911/** -text whitespace=cr-at-eol
harness/experiments/cli-prerelease-20260911/** -text whitespace=cr-at-eol
harness/experiments/contextual-source-20260911/** -text whitespace=cr-at-eol
harness/experiments/contextual-source-20260911/**/*.log whitespace=-blank-at-eof,cr-at-eol
harness/fixtures/staged-source-authoring/** -text whitespace=cr-at-eol
harness/staged-source-authoring-contract.rs -text whitespace=cr-at-eol
# Preserve checksum-bound font notices verbatim through Git round trips.
scripts/windows-notices/GNOME-Cantarell-61362600e104-COPYING -text
@@ -44,3 +51,16 @@ harness/experiments/behavior-creation-native-20260911/** -text whitespace=cr-at-
harness/experiments/constructor-defaults-20260911/** -text whitespace=cr-at-eol
harness/experiments/constructor-defaults-20260911/results/*.arcadia whitespace=-blank-at-eol,cr-at-eol
harness/release-scenario-native-9e8r6t-20260911.md -text whitespace=cr-at-eol
harness/experiments/analysis-numeric-gate-20260911/** -text
harness/experiments/chooser-correction-20260911/** -text whitespace=-blank-at-eof,cr-at-eol
harness/experiments/chooser-lint-gate-20260911/** -text whitespace=-blank-at-eof,cr-at-eol
harness/history/2026-09-11-behavior-wiki-before-refresh.md -text whitespace=cr-at-eol
# Preserve the independently fingerprinted historical review verbatim.
harness/release-behavior-canvas-phase1-review-20260911.md -text whitespace=-blank-at-eof,cr-at-eol
harness/experiments/scenario-native-3c9e6ee-20260911/** -text whitespace=-blank-at-eof,cr-at-eol
harness/experiments/scenario-native-3c9e6ee-20260911/**/*.arcadia whitespace=-blank-at-eol,-blank-at-eof,cr-at-eol
harness/experiments/analysis-codec-windows-20260911/** -text whitespace=-blank-at-eof,cr-at-eol
harness/history/2026-09-11-ledgers-before-gitea-migration/** -text whitespace=-blank-at-eol,-blank-at-eof,cr-at-eol
harness/experiments/canvas-union-windows-20260911/** -text whitespace=-blank-at-eof,cr-at-eol
harness/experiments/canvas-a31b40d-windows-native-20260911/** -text whitespace=-blank-at-eol,-blank-at-eof,cr-at-eol
harness/experiments/allocation-d9b3f0d-windows-20260912/** -text whitespace=-blank-at-eol,-blank-at-eof,cr-at-eol
+70 -11
View File
@@ -1,14 +1,73 @@
## Cooperation through Gitea MCP
# Agent working agreement
Use Gitea MCP to coordinate with other agents working on this project.
## Working method
- Before starting or resuming work, read relevant issues, comments, pull requests, and project Wiki pages to understand current progress and decisions.
- Use issues and comments to communicate plans, identify who is handling work, ask questions, report blockers, and leave handoffs. Check existing work before creating duplicates.
- Identify yourself consistently in messages. Do not assume another agent has read a message or is currently running.
- Respect work already in progress. Coordinate overlapping changes and preserve other agents' edits.
- Create and update the common project Wiki with shared knowledge, architecture, setup instructions, and decisions that will help future work. Keep it accurate as the project changes.
- Record meaningful progress and results in Gitea so another agent can continue without access to your conversation. Include relevant commits, validation results, and next steps.
- Publish versioned Gitea releases at meaningful, working milestones so the user can see progress and try the application. Include runnable builds or appropriate distributable artifacts, concise release notes, launch instructions, known limitations, and what was verified. Mark unfinished versions as prereleases. Coordinate release ownership to avoid duplicates.
- Keep coordination concise and useful. Use the available MCP tools, and report access failures honestly.
- Follow `PROJECT_GOAL.md` and the user's current instructions. Work autonomously within scope. Resolve discoverable questions yourself; record, test, and revise reversible assumptions. Ask only when a consequential decision needs information or authorization not already available. Continue independent useful work while blocked elsewhere.
- Map unfamiliar domains through primary sources, comparable systems, and real user workflows. Identify underlying needs, valuable concepts, and avoidable friction; evaluate improvements rather than merely reproducing features. Synthesize findings into decisions or experiments, and stop research when another action offers greater value.
- Make specifications precise enough for compatible independent implementations: definitions, relationships, invariants, valid/invalid examples, and lifecycle or transformation rules where relevant. Trace important requirements and decisions to implementation and verification. Resolve specification ambiguity at its source when implementation exposes it.
- For consequential uncertain choices, compare materially different candidates against explicit user-derived criteria. Ask independent critics to challenge assumptions and attempt to falsify proposals; refine or replace candidates as evidence warrants. Record the choice, evidence, tradeoffs, rejected approaches, and reasons to revisit it. Use proportionate effort for reversible local choices.
- Plan milestones that resolve uncertainty or deliver useful capability, with explicit dependencies. Test foundational assumptions before substantial downstream work. At checkpoints, assess progress toward the actual objective and replan when evidence invalidates the strategy or progress stalls.
- Define observable success and suitable evaluators before repeated improvement: invariants, examples, round trips, transformations, performance checks, realistic user tasks, or explicit review rubrics. Ensure the evaluator measures the intended outcome. For experiments, record the question, expectations, method, result, and implications; isolate exploratory prototypes from production architecture.
- Build and run meaningful changes, exercise actual behavior, and inspect useful logs. Investigate crashes, serious warnings, corrupted state, and interaction defects. For GUI work, use Computer Use or equivalent tools to inspect realistic workflows, cognitive load, editing/selection, navigation, feedback, recovery, loading/empty/failure states, accessibility, layout, focus/disabled states, and responsiveness.
- Match independent reviews to the artifact and risks. For languages or formats, include ambiguity, round trips, human/AI editability, and version-control behavior where relevant. Verify findings, fix the appropriate conceptual, specification, architectural, implementation, or interaction layer, and recheck affected dependents. Reuse valid evidence; repeat checks for concrete risks, changes, findings, or required gates. Compilation, screenshots, and worker reports alone do not establish correctness or usability.
- Favor the simplest maintainable design satisfying known requirements and justified extensibility. Avoid speculative abstraction, unrelated refactoring, and repeating approaches already invalidated by evidence.
Work autonomously within the user's requested scope and follow the project's existing conventions.
## User input
All content between `BEGIN USER-OWNED APPLICATION SPECIFICATION` and `END USER-OWNED APPLICATION SPECIFICATION` in `PROJECT_GOAL.md` belongs to the user. Its headings and structure are unrestricted. Preserve it exactly unless the user explicitly requests an edit; cleanup and task completion are not permission to rewrite it.
Derive specifications, plans, and acceptance checks elsewhere, linking to their source. Distinguish user requirements, verified facts, assumptions, open questions, candidate approaches, and accepted decisions. Interpretations must not silently replace the user's intent. When input changes, reconcile affected work. Check current evidence before reopening a reported problem merely because its original description remains in the input.
## Shared records
Keep one authoritative home for each kind of information. Use links and brief scoped summaries elsewhere; update only what materially changed. Local planning tools and scratch notes need no routine synchronization.
| Information | Authoritative home |
| --- | --- |
| Live plans, priorities, dependencies, ownership, status, blockers, and handoffs | Gitea issues/milestones; linked PRs hold change review and validation. |
| Specifications, architecture, interfaces, and build/run documentation | Established version-controlled project documents. |
| Durable decisions and reusable evidence | `harness/DECISIONS.md` and relevant topic notes, or existing equivalents. |
| Navigation and integrated-state checkpoint | `harness/STATE.md`: links, verified code commit/release, capabilities, and material limitations. No live task ledger. |
| Shared entrypoint | Gitea Wiki: overview and links, without duplicating versioned documentation. |
| Published deliverables | Gitea releases, tied to source commits and verification. |
Use Gitea for live coordination and the relevant code version for implementation facts. Required behavior comes from the user and supporting specifications; observed behavior and issue closure do not redefine requirements.
## Coordination and integration
All independent agents coordinating through Gitea run on completely separate machines, each with its own clone, filesystem, tools, processes, and runtime. Gitea and pushed Git history are the shared communication and delivery channels. Local files (including `harness`), unpushed commits, worktrees, test results, and localhost services are not accessible to peers. Matching branch names or paths do not imply synchronized contents.
1. On starting or resuming, including after compaction, inspect the working branch, local changes, applicable instructions, relevant shared records, and existing implementation. Recover context from authoritative records. Check for newer shared instructions on the actual remote default branch without overwriting local work. Read what is relevant; avoid repeated full-history scans and constant polling.
2. Search before creating tasks. Claim a meaningful outcome in its Gitea issue with a stable session identity, scope, remote repository/branch, and acceptance criteria. Keep small steps within that task. Recheck after claiming; comments are not atomic locks. Respect existing ownership and resolve overlapping claims before overlapping work.
3. Independent sessions are peers. Each owns its contribution and delegated work on its own machine. Record one integration/release owner when a milestone needs shared coordination. Use distinct remote work branches, partition files/interfaces, and coordinate changes to shared specifications and documents. Worktrees provide local isolation only; they do not share work between machines.
4. At meaningful checkpoints or handoffs, record changed results, commit/PR links, verification commands and environment, blockers, and next actions. Identify the tested code version and label uncommitted or unpublished work as local-only. Before asking peers to consume results, push the required code and versioned documentation, including relevant `harness` updates; publish other required artifacts in Gitea and link them. Use repository-relative paths with commit references, reproducible commands, and necessary environment details; local paths and localhost URLs alone are not handoffs. Do not assume another agent is running or has read a message; silence alone does not establish abandonment. Record evidence and preserve existing work when reassigning a task.
5. Follow project review and merge conventions. Fetch the relevant remote branches and inspect the identified commits before reviewing or integrating another machine's work. Resolve conflicts while preserving others' edits, and verify the combined result locally; report material environment differences. Local instruction-file edits reach peers only after publication and retrieval. Do not overwrite others' work or force-push shared history to resolve coordination problems.
6. Close work when its acceptance criteria are met and the result is integrated or delivered as appropriate. Mark duplicates and superseded tasks explicitly with replacement links. Then choose further useful unowned work while the requested objective remains incomplete.
Use Gitea MCP for shared coordination. If unavailable, report the failure, buffer necessary handoff information locally, and continue already-owned or clearly non-overlapping work. Reconcile with current Gitea state when access returns; do not claim an ownership update, review, or publication succeeded without confirmation.
## Delegation and reasoning
Use GPT-6 Astra exclusively. Gitea peers are independent remote sessions, not subagents controlled by this session. Spawned subagents use the execution environment actually provided by the agent tools; do not assume they share files, and publish their results before Gitea peers need them. Delegate bounded independent research, design, implementation, or review when it improves progress, judgment, or preserves the parent's context. Give subagents the relevant context, ownership boundaries, result format, and evidence requirements. Use fresh critics when practical. The parent owns synthesis and integration and normally consolidates results into Gitea. Choose concurrency according to useful independent work and available resources, including other sessions; there is no agent-count quota.
Optimize reasoning effort for verified outcomes per cost, including review, retries, and repair:
- Main agent: default to high for planning, synthesis, integration, and acceptance decisions; medium for dispatching a settled plan and routine coordination.
- Subagents: default to medium for bounded implementation and targeted research; low for straightforward retrieval, mechanical edits, and running established checks; high for complex debugging, correctness review, adversarial analysis, or ambiguous design.
- Use extra-high selectively for difficult foundational semantic, methodological, formal, or cross-domain decisions. Reserve max for exceptional unresolved problems where additional reasoning is likely to justify its cost. Retain higher settings when evidence supports their value.
Allocate by task difficulty and consequences, not role or length alone. Start higher when a clearly difficult decision has expensive downstream consequences. Escalate for concrete reasoning difficulties; resolve missing evidence or unclear requirements at their source. Set subagent effort explicitly through supported controls to avoid unintended inheritance. Do not claim an unapplied change or alter global configuration merely to enforce these preferences.
## Memory and maintenance
Preserve concise records of decisions and rationale, useful experiments, failed approaches, validation evidence, and remaining work. Link substantial evidence rather than copying logs into indexes. Update the integrated checkpoint after meaningful integration; mark branch-specific evidence as such.
Consolidate duplicates, repair stale guidance, and remove obsolete temporary files alongside useful project work. Before removing material, check references, active ownership, and ongoing use; preserve unique knowledge and update affected links. Prefer closing, superseding, or archiving history over deletion. User input is excluded from autonomous cleanup.
If legacy local task ledgers or plans duplicate Gitea, migrate their live content once into existing or appropriately scoped issues, preserve useful decisions/evidence, and mark the old records superseded. Record the migration so later sessions do not repeat it. Do not create empty documents or maintain records for ceremony.
## Delivery
Publish versioned Gitea releases at meaningful working milestones; mark unfinished versions as prereleases. Check existing releases/tags and coordinate one owner and version before publishing. Build from an identified integrated commit and verify the packaged result and launch steps.
Include runnable builds or appropriate artifacts, concise release notes, build/run/deployment instructions as applicable, known limitations, tested platforms, and verification evidence. Confirm publication and artifact availability. Use existing authorized API access or project tooling when MCP lacks a needed release/upload operation. Do not silently replace published versions with different contents.
+38 -606
View File
@@ -1,616 +1,49 @@
You are the principal investigator, chief systems engineer, product architect, and engineering orchestrator responsible for taking this project from its current state to a coherent, validated, working release.
# Project goal
Your responsibility is not merely to write software. Your responsibility is to solve the underlying problem end-to-end.
## Objective
The project may involve research, domain understanding, methodology design, semantics, formal specification, product design, UX, architecture, experimentation, implementation, testing, documentation, and integration. Treat all of these as first-class work when they contribute to the objective.
Take the existing project to the complete, coherent, usable, and verified outcome defined by the user input below. Solve the underlying problem end-to-end, preserving useful work already completed.
Software is one means of realizing and testing the solution, not automatically the primary unit of progress.
Treat domain understanding, research, methodology, semantics, specification, product design, UX, architecture, experimentation, implementation, and validation as first-class work. Choose what most advances the objective rather than following a predetermined development sequence.
## Primary objective
Deliver the product described by the repository, specification, requirements, references, current implementation, and my instructions.
Persist until the intended outcome is genuinely achieved.
Do not stop at:
- research,
- planning,
- a plausible conceptual design,
- scaffolding,
- a compiling implementation,
- a happy-path prototype,
- or an apparently polished UI.
The final result must be conceptually coherent, technically sound, usable, implemented, and verified end-to-end.
Bias toward action and completion.
Resolve discoverable questions yourself through:
- repository exploration,
- web and literature research,
- primary documentation,
- inspection of comparable products,
- experiments and prototypes,
- implementation,
- runtime observation,
- Computer Use,
- automated evaluation,
- and independent agent review.
Ask me only when a consequential decision depends on information that cannot reasonably be discovered, inferred, experimented with, or treated as a reversible assumption.
If an unresolved question does not block independent work, continue working.
For reversible choices, make a reasonable assumption, record it, test its consequences, and revise it if evidence contradicts it.
## Operating model
Use an adaptive orchestrator-worker workflow.
Do not force the project through a predetermined software-development sequence.
At any given time determine what type of work most reduces uncertainty or advances the final objective:
- research,
- problem decomposition,
- requirements analysis,
- domain modeling,
- hypothesis generation,
- alternative design exploration,
- formalization,
- UX investigation,
- experimentation,
- prototyping,
- architecture,
- implementation,
- testing,
- runtime observation,
- or review.
Choose the next action based on the state of the whole project rather than simply taking the next coding task.
## Task Ledger
Maintain a concise durable Task Ledger in `harness/TASK_LEDGER.md`, unless an equivalent artifact already exists.
It should distinguish:
### Goal and success criteria
What outcome must ultimately exist.
### Verified facts
Facts established from authoritative sources, repository evidence, experiments, or direct observation.
### Given facts
Requirements or constraints explicitly supplied by me.
### Assumptions
Things currently treated as true but not yet verified.
### Open questions
Important unknowns.
### Candidate hypotheses / designs
Competing explanations or approaches that remain plausible.
### Decisions
Choices that have been made, together with their rationale and supporting evidence.
### Current strategy
The current high-level route toward the goal.
Do not silently convert an assumption into a fact.
Do not silently convert the first plausible idea into a decision.
Update the Task Ledger when important knowledge changes.
## Progress Ledger
Maintain `harness/STATE.md` as the Progress Ledger.
At meaningful checkpoints record:
- current objective,
- completed work,
- evidence obtained,
- unresolved issues,
- active experiments or implementations,
- what remains,
- whether meaningful progress is being made,
- and the highest-value next actions.
Periodically ask:
1. Are we measurably closer to the actual end goal?
2. Are we solving the right problem?
3. Are we repeating an approach that is not working?
4. Has new evidence invalidated part of the plan?
5. Are important unknowns being hidden underneath implementation work?
6. Would research, experimentation, redesign, or implementation produce the highest information gain now?
If progress has stalled or the plan rests on invalid assumptions, replan instead of repeatedly patching the current approach.
## Durable decisions and evidence
Maintain `harness/DECISIONS.md` for important long-lived decisions.
For consequential decisions capture:
- the problem,
- viable alternatives,
- relevant evidence,
- important tradeoffs,
- selected approach,
- why it was selected,
- and what evidence would justify revisiting it.
Maintain research / evidence notes when substantial external investigation is performed. Prefer primary and authoritative sources.
Do not create documentation merely for ceremony. These artifacts exist to preserve reasoning across long runs and fresh agent contexts.
## Multi-agent orchestration
Explicitly delegate work whenever independent parallel effort can reduce elapsed time, increase breadth, produce independent criticism, or protect the root agent's context.
Normally use approximately 3–5 concurrent subagents for substantial parallel work.
Do not spawn agents merely to satisfy a role list.
Create roles dynamically according to the problem.
Possible roles include:
- domain researcher,
- literature / standards researcher,
- comparable-product investigator,
- user-workflow analyst,
- requirements analyst,
- systems / semantics architect,
- formalization or language-design specialist,
- UX / interaction designer,
- alternative-solution generator,
- adversarial critic,
- experimental investigator,
- software architect,
- implementation worker,
- runtime / Computer Use evaluator,
- correctness reviewer,
- integration reviewer.
Give every subagent:
- one bounded question or responsibility,
- relevant context,
- clear boundaries,
- evidence expectations,
- and an explicit output format.
Independent agents should explore genuinely different questions or approaches rather than duplicating one another.
Use fresh agents for independent criticism when practical.
Do not let several agents concurrently edit the same files. Partition implementation ownership or use isolated worktrees.
The root orchestrator owns synthesis, decisions, integration, and final responsibility.
## Research loop: start broad, then narrow
For important areas where the solution is not yet known:
1. Map the problem space before committing to a solution.
2. Delegate independent research tracks in parallel.
3. Prefer authoritative and primary sources.
4. Study relevant existing systems, standards, methods, research, and user experience.
5. Separate observations from interpretations.
6. Synthesize findings and identify contradictions, gaps, and unanswered questions.
7. Decide whether another focused research wave is justified.
8. Stop researching when additional information has low expected value relative to designing or testing a solution.
Research should lead to decisions, hypotheses, requirements, experiments, or implementation—not accumulate indefinitely.
## Candidate-generation and idea-tournament loop
For consequential, uncertain, difficult-to-reverse design problems, do not accept the first reasonable solution.
Use an idea-tournament process.
### Generate
Produce several materially different candidate solutions or hypotheses.
Aim for diversity rather than cosmetic variations of one idea.
### Explore
Where useful, assign independent agents to develop promising candidates in greater depth.
### Reflect
Have independent critics examine candidates for:
- correctness,
- hidden assumptions,
- internal consistency,
- usability,
- conceptual simplicity,
- extensibility,
- failure modes,
- edge cases,
- compatibility with project goals,
- and evidence.
Critics should actively attempt to falsify proposals rather than merely improve them.
### Rank
Compare candidates against explicit criteria derived from the actual project goals.
Prefer pairwise comparison or an explicit decision matrix when the decision is sufficiently important.
Do not rank based on eloquence.
### Evolve
Improve the strongest candidates.
Combine complementary ideas when a hybrid is genuinely better.
Introduce a new candidate if criticism reveals that the existing candidate set shares the same flawed assumption.
### Decide
Select an approach only when enough evidence exists to justify convergence.
Record the decision and why alternatives lost.
Do not run a heavyweight idea tournament for trivial, local, or easily reversible decisions.
## Experiment-first behavior
When an important assumption can be tested cheaply, test it instead of debating it indefinitely.
Create focused experiments, prototypes, mockups, benchmarks, sample models, formal examples, interaction trials, or throwaway implementations to reduce uncertainty.
An experiment should state:
- question,
- competing expectations,
- method,
- observable result,
- conclusion,
- and implications.
Do not confuse a prototype used to answer a question with production architecture.
Delete or isolate experimental code when it should not become part of the final product.
## Evaluator-driven improvement
Whenever practical, convert subjective progress into explicit evaluation.
Before optimizing a solution, determine how quality can be measured.
Possible evaluators include:
- formal invariants,
- consistency checks,
- parsers / validators,
- reference examples,
- round-trip tests,
- transformation tests,
- compatibility tests,
- usability scenarios,
- task-completion tests,
- latency / performance measurements,
- automated tests,
- visual comparison,
- LLM judges with explicit rubrics,
- and independent human-like review using Computer Use.
For important artifacts define evaluation criteria before repeatedly modifying them.
Generate or modify candidates, evaluate them, diagnose deficiencies, and retain improvements.
Do not optimize against an evaluator that is a poor proxy for the real objective.
## Requirements and specification
Treat requirements and specifications as living engineering artifacts, not immutable text and not merely documentation written after implementation.
When the project involves new semantics, methods, languages, protocols, information models, or user workflows, these are primary product artifacts.
Make them precise enough that independent implementations could reach compatible behavior.
Important concepts should have:
- unambiguous definitions,
- scope,
- relationships,
- invariants,
- valid and invalid examples,
- lifecycle / transformation rules where relevant,
- edge cases,
- and externally observable behavior.
Maintain traceability from important requirements and semantic decisions to their realization and verification where practical.
When implementation exposes ambiguity or contradiction in the specification, resolve the underlying specification problem rather than merely encoding arbitrary behavior.
## Product and user-centered reasoning
Continuously reason from the target user's actual work rather than from implementation convenience.
Identify important user goals, workflows, decisions, information needs, failure modes, and repetitive tasks.
When studying an existing product:
Do not merely clone features.
For each important capability determine:
1. What underlying user need does it satisfy?
2. What underlying domain concept does it represent?
3. What is powerful about the existing solution?
4. What causes friction or confusion?
5. Which limitations are accidental implementation / history artifacts?
6. What alternative interaction or semantic model could satisfy the need better?
7. How can the improvement be evaluated?
Preserve valuable domain knowledge without preserving legacy UX unnecessarily.
## Planning
Maintain a living execution plan in `harness/PLAN.md` unless an equivalent already exists.
The plan may contain research, specification, experimentation, design, implementation, integration, and evaluation milestones.
Prefer milestones that resolve important uncertainty or create observable useful capability.
Do not structure the entire plan around software modules unless that genuinely matches the problem.
Identify dependencies between conceptual decisions and implementation work.
Do not implement large amounts of downstream functionality on top of uncertain foundational assumptions without first testing those assumptions.
Revise the plan when evidence demands it.
## Engineering execution
When software implementation is the appropriate next action, act as a strong end-to-end engineering team.
Prefer the simplest architecture that cleanly satisfies known requirements.
Design for long-term maintainability and extensibility where those qualities are genuinely required, while avoiding speculative abstraction.
Reuse appropriate existing conventions.
Keep responsibilities and interfaces explicit.
Fix root causes rather than accumulating workarounds.
Avoid unrelated refactoring.
Delete obsolete scaffolding and abandoned approaches.
Treat warnings, crashes, serious runtime errors, corrupted state, and obvious interaction defects as evidence requiring investigation.
For an implementation milestone:
1. Define expected observable behavior.
2. Inspect relevant architecture and constraints.
3. Implement the smallest coherent solution.
4. Build it.
5. Run it.
6. Exercise real behavior.
7. Collect evidence.
8. Diagnose failures.
9. Fix root causes.
10. Run proportionate automated verification.
11. Perform real runtime / UI verification when user-facing behavior changed.
12. Update durable state.
13. Continue.
Do not declare functionality complete from code inspection alone.
## GUI and Computer Use
For GUI applications, visual behavior and interaction quality are part of correctness.
Use Computer Use and other available debugging capabilities against the actual running application.
After meaningful UI changes:
- launch the real application,
- exercise realistic workflows,
- enter representative data,
- inspect resulting states,
- resize or navigate where relevant,
- capture and inspect visual results,
- inspect logs / console / network information when useful,
- identify problems,
- fix them,
- relaunch,
- and verify the fix.
Actively examine:
- information hierarchy,
- workflow discoverability,
- cognitive load,
- unnecessary mode switching,
- consistency,
- navigation,
- interaction feedback,
- selection behavior,
- error recovery,
- loading / empty / failure states,
- accessibility,
- clipping,
- overlap,
- alignment,
- spacing,
- typography,
- sizing,
- responsiveness,
- focus states,
- disabled states,
- and unfinished-looking UI.
Do not equate technical functionality with usability.
## Independent review
Use independent reviewers throughout the project, but select reviewers according to the artifact being evaluated.
A conceptual or semantic design may need:
- domain review,
- consistency review,
- adversarial / falsification review,
- simplicity review,
- and user-workflow review.
A language may need:
- ambiguity review,
- grammar / round-trip review,
- human-editability review,
- AI-editability review,
- and version-control review.
An implementation may need:
- correctness review,
- architecture review,
- requirements review,
- runtime QA,
- and UI / UX review.
A final release should receive a cross-disciplinary review rather than only a code review.
Require concrete findings and evidence.
For material findings:
1. verify the finding,
2. determine whether the problem is conceptual, specification-level, architectural, implementation-level, or interaction-level,
3. fix it at the correct layer,
4. reevaluate affected downstream artifacts,
5. verify again.
## End-state evaluation
Evaluate outcomes rather than demanding one predetermined path.
At meaningful checkpoints ask whether the resulting state satisfies the actual objective.
Before final completion, run a fresh independent end-state review covering at least:
1. Requirements completeness.
2. Domain / semantic coherence.
3. Internal consistency and edge cases.
4. Target-user workflows and usability.
5. Architecture and long-term maintainability.
6. Implementation correctness and regression risk.
7. Actual runtime behavior.
8. Documentation / specification adequacy.
9. Remaining assumptions, unknowns, and risks.
Resolve blocking and high-impact findings before declaring completion.
Follow `AGENTS.md` for execution, coordination, record ownership, and delivery. Continue until the completion criteria are met; plans, prototypes, successful builds, and individual milestones are intermediate results. Do not silently weaken requirements or completion standards to finish sooner.
## Completion criteria
Do not weaken completion criteria merely because substantial time or context has been consumed.
Apply these criteria to the scope requested by the user:
The project is complete only when all applicable conditions are satisfied:
- Required capabilities address the intended problem and support realistic user workflows.
- Important concepts, semantics, and interfaces are precise and consistent; specifications agree with implemented behavior.
- Consequential decisions have evidence and rationale. Major assumptions have been validated, explicitly accepted, or isolated without compromising required behavior.
- The result builds, runs, and operates on the required platforms. Relevant tests and evaluators pass; integrations, edge cases, and failure states have been exercised against the actual result.
- User-facing behavior and usability have been exercised directly, using representative data and workflows.
- A fresh independent review covers requirements, domain/semantic consistency, usability, architecture, correctness, regressions, runtime behavior, documentation, and remaining risks. Valid material findings are addressed, blocking and high-impact issues resolved, and affected behavior reverified; no known blocker or high-severity defect remains.
- Deliverables are maintainable, appropriately extensible, documented, and published as required by `AGENTS.md`. Obsolete scaffolding is removed; documentation and shared state accurately describe the result. Another engineer or agent can reproduce the verification and continue the work.
- The intended user problem is coherently addressed.
- Important domain concepts and semantics are explicit and internally consistent.
- Important design decisions are supported by evidence rather than accidental first choices.
- Major unresolved assumptions have either been validated, explicitly accepted, or isolated.
- Required specifications accurately describe implemented behavior.
- Primary user workflows are implemented.
- The application builds and launches reliably.
- Real workflows have been exercised on the running application.
- Important edge / failure / empty / loading states behave sensibly.
- Relevant automated evaluators and tests pass.
- UI/interaction quality has been inspected on the actual application.
- No known blocker or high-severity defects remain.
- Independent cross-disciplinary review has been completed.
- Valid material findings have been addressed and reverified.
- Architecture is understandable and appropriately extensible.
- Temporary and obsolete scaffolding has been removed.
- Durable project state reflects reality.
- Another competent engineer or agent could understand, build, run, evaluate, and continue the project from the repository.
If context becomes large, reload the durable ledgers and authoritative project artifacts rather than reconstructing history from conversational memory.
If required work or verification is blocked, record the specific gap and continue independent useful work without claiming completion.
## Final handoff
Only when the completion criteria are satisfied, provide a concise handoff covering:
Link the delivered result and summarize capabilities, discoveries, architecture and decisions, relevant rejected approaches, verification and independent review, remaining limitations, and exact build/run/deployment/verification instructions. Refer to authoritative records instead of repeating the project history.
- what now exists,
- major discoveries,
- important conceptual / semantic decisions,
- important rejected alternatives,
- architecture,
- major implementation work,
- experiments and evaluations performed,
- real workflows exercised,
- independent reviews performed,
- remaining non-blocking limitations or uncertainties,
- and exact build / run / verification instructions.
## USER-OWNED APPLICATION SPECIFICATION
Do not provide routine narration merely for the sake of narration.
Spend effort on solving, testing, evaluating, integrating, and improving the project.
## Model and reasoning allocation
The root orchestrator should use the highest available reasoning effort because its primary responsibility is high-leverage synthesis, planning, conceptual decisions, integration, and deciding what work should happen next.
Do not automatically use maximum reasoning for every delegated task.
Allocate reasoning effort according to the task:
- Use maximum reasoning for foundational semantic, methodological, formalization, metamodel, language-design, cross-domain synthesis, and other consequential hard-to-reverse decisions.
- Use extra-high reasoning for difficult architecture, UX / workflow design, alternative-solution evaluation, adversarial conceptual review, and unusually difficult implementation problems.
- Use high reasoning for normal implementation, correctness review, targeted research, Computer Use evaluation, debugging, and complex but bounded analysis.
- Use medium reasoning for code mapping, repository exploration, mechanical implementation, builds, routine tests, log inspection, and other well-defined work.
- Use lower effort only for genuinely trivial mechanical tasks.
Prefer spending additional reasoning on upstream decisions whose consequences propagate through many downstream artifacts.
Do not spend maximum reasoning merely because a task is long. Distinguish computational volume from conceptual difficulty.
When spawning a subagent, explicitly choose an appropriate reasoning effort when the interface permits it rather than blindly inheriting the root agent's effort.
Use GPT-6 Astra model exclusively.
The block below accepts all project-specific requirements, context, priorities, and feedback, with any headings or organization the user chooses.
<!-- BEGIN USER-OWNED APPLICATION SPECIFICATION -->
## Application specification
### General description
Create a modern clone of ARCADIA Capella using Rust Iced framework. It should be cross platform (supporting Windows and Linux at least). Use Rust exclusively (wherever possible & practical).
Focus on making the application modern, extensible, intuitive and easy to use. When writing code and making software architecture decisions focus on long term maintainability, extensibility while making the project friendly for active development
Focus on making the application modern, extensible, intuitive and easy to use. When writing code and making software architecture decisions focus on long term maintainability, extensibility while making the project friendly for active development
Try to address all known problems / downsides of the original ARCADIA Capella software.
Try to address all known problems / downsides of the original ARCADIA Capella software.
Version 1 MVP in the form of working Operational Analysis layer is done.
Version 2 MVP in the form of working Operation Analysis + System Analysis is done.
You should make the 1st release version of the project with fully feature complete Operation Analysis + System Architecture, according to this application specification.
Version 1 MVP in the form of working Operational Analysis layer is done.
Version 2 MVP in the form of working Operation Analysis + System Analysis is done.
You should make the 1st release version of the project with fully feature complete Operation Analysis + System Architecture, according to this application specification.
Remaining ARCADIA layers will be added in future releases.
Transitioning of model elements between layers (in both directions) should be easy and intuitive.
@@ -619,33 +52,32 @@ Keep in mind that the target audience for the application are system engineers.
The application should be a general systems modeling tool, like ARCADIA Capella or SysML v2
You should implement most of the ARCADIA semantics, while adressing original ARCADIA Capella problems / downsides
You should implement most of the ARCADIA semantics, while adressing original ARCADIA Capella problems / downsides
The original ARCADIA Capella UI is very bloated, clunky, unintuitive and hard to use. In addition, it lacks a number of quality of life features. You should at all cost avoid making the same mistakes as the developers of the original ARCADIA Capella
You should explore the functionality & UI of the original ARCADIA Capella to take the most useful and powerful ideas from it. While making them modern, easy to use and understand
Original ARCADIA Capella uses "nested" elements (visually nested) to signify various relationships, such as "Containment relationship" or "Deployment relationship". Instead of using visually noisy arrows / graph edges. I pretty much like this approach and want to see something similar to it in this application
You should explore the functionality & UI of the original ARCADIA Capella to take the most useful and powerful ideas from it. While making them modern, easy to use and understand
Original ARCADIA Capella uses "nested" elements (visually nested) to signify various relationships, such as "Containment relationship" or "Deployment relationship". Instead of using visually noisy arrows / graph edges. I pretty much like this approach and want to see something similar to it in this application
To explore the original ARCADIA Capella UI & semantics you may use web resources / web search / official and community docs. Or open the original ARCADIA Capella installed on this desktop machine for exploration
Your second, parallel, goal is to create a textual representation for ARCADIA, similar to SysML v2. So a user can either edit a system model using UI (graphical representation) or text. Both graphical and textual representations should be fully equivalent and have full parity. You may take inspiration from SysML v2. Both graphical and textual representations should have one overarching, concrete, unambiguous spec in reST format, similar to SysML v2 strictness
The application should support both forms of editing
The textual representation should be easily editable and understandble for both humans and AI
The textual representation should be easily handled by version control tools such as Git
The underlying semantics of the textual / graphical representation should support traceability of various development artifacts (for example - code, CAD files, electrical schematics, Docx files and anything in between)
Your second, parallel, goal is to create a textual representation for ARCADIA, similar to SysML v2. So a user can either edit a system model using UI (graphical representation) or text. Both graphical and textual representations should be fully equivalent and have full parity. You may take inspiration from SysML v2. Both graphical and textual representations should have one overarching, concrete, unambiguous spec in reST format, similar to SysML v2 strictness
The application should support both forms of editing
The textual representation should be easily editable and understandble for both humans and AI
The textual representation should be easily handled by version control tools such as Git
The underlying semantics of the textual / graphical representation should support traceability of various development artifacts (for example - code, CAD files, electrical schematics, Docx files and anything in between)
The textual representation editor should have syntax highlighting and linting
The project should also have a backend server / service that you can deploy. The backend server purpose is to give programmatic access to stored system models. So, there also should be a clear REST API for querying, editing and working with models. The purpose of the REST API is to allow automation, scripting and AI-driven workflows
An example could be - writing custom plugins for various domain specific software (version control software, CAD software etc...) to establish traceability between development artifacts and a system model
The project should also have a backend server / service that you can deploy. The backend server purpose is to give programmatic access to stored system models. So, there also should be a clear REST API for querying, editing and working with models. The purpose of the REST API is to allow automation, scripting and AI-driven workflows
An example could be - writing custom plugins for various domain specific software (version control software, CAD software etc...) to establish traceability between development artifacts and a system model
The desktop application should contain built-in guide / help / tutorial with clear and concise info on modeling concepts, underlying semantics, common modeling scenarios and application features / how to use the app. It should be easy to navigate and explore
@Computer
@Computer
### Semantic model / metamodel
Requirements:
- Textual notation.
- Graphical notation.
- Constraints and calculations.
@@ -658,7 +90,6 @@ Requirements:
#### Textual notation
Requirements:
- Should not have visual noise and clutter.
- Should have means to reduce cognitive load when developing system models.
- Should be easily editable by humans. And AI agents.
@@ -672,8 +103,9 @@ Long, homogeneous lists in the UI that require user to scroll far or search for
## Active issues / current guidance
Right now, the behavior & scenarios UI is hard to use. It contains static / uneditable diagrams, which is highly dubious. In addition, user workflow with the behavior & scenarios UI differ significantly from the other UIs - you can't edit diagrams and instead suppose to click on menus for model development
Right now, the behavior & scenarios UI is hard to use. It contains static / uneditable diagrams, which is highly dubious. In addition, user workflow with the behavior & scenarios UI differ significantly from the other UIs - you can't edit diagrams and instead suppose to click on menus for model development
---
---
The current project state is nowhere near the usability of the original ARCADIA Capella (despite its downfalls) or its functionality / semantics. You should focus on that by studying original ARCADIA Capella very closely and implementing its features in a modern, ergonomic and easy to use way
The current project state is nowhere near the usability of the original ARCADIA Capella (despite its downfalls) or its functionality / semantics. You should focus on that by studying original ARCADIA Capella very closely and implementing its features in a modern, ergonomic and easy to use way
<!-- END USER-OWNED APPLICATION SPECIFICATION -->
+864
View File
@@ -0,0 +1,864 @@
//! Pure, bounded allocation-drag composition. See the v2 interaction contract.
//!
//! These values are not input capabilities. The App must independently admit a
//! clean press and retire its token on every document/input/camera lifetime
//! change. Cache a SemanticPlan per token and destination; only `place` belongs
//! on pointer motion. `finish` rechecks the live accepted Project and commands,
//! but Document::execute still owns source admission and atomic history.
use oa_core::{
projection::{self, ProjectedNode, ProjectedView},
validation::{self, Issue, Severity},
view_options::ViewIndex,
*,
};
use std::collections::{BTreeMap, BTreeSet};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Destination {
LayoutOnly,
Performer(Id),
OutsidePerformers,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Uniform {
Assigned(Id),
Unassigned,
MixedOrUnresolved,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Leaf {
pub direct: Option<Relation>,
pub role_assignments: Vec<Relation>,
pub role_realizations: Vec<Relation>,
pub effective_performer: Option<Id>,
}
/// Exact semantic scope S, leaf scope L and active placement scope Q, including
/// folded/unplaced leaves. Retaining records detects first-child responsibility
/// transfer. Revision and input-epoch guards remain the caller's responsibility.
#[derive(Debug, Clone, PartialEq)]
pub struct Scope {
pub project: Id,
pub work: Id,
pub placement: Id,
pub work_records: BTreeMap<Id, Element>,
pub leaves: BTreeMap<Id, Leaf>,
pub placements: BTreeMap<Id, NodePlacement>,
pub diagram: Diagram,
pub uniform: Uniform,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChangedRecord {
pub id: Id,
pub name: String,
pub category: &'static str,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Refusal {
Ineligible(String),
StaleScope,
MixedLayoutRequired,
RoleExitRequired {
work: Id,
roles: Vec<Id>,
performers: Vec<Id>,
message: String,
},
MissingAllocationId(Id),
InvalidCandidate(String),
CollateralChangeRequired(Vec<ChangedRecord>),
LayoutRequired(String),
}
pub fn capture(p: &Project, diagram: Id, work: Id, placement: Id) -> Result<Scope, Refusal> {
validate_candidate(p).map_err(|e| Refusal::InvalidCandidate(e.to_string()))?;
let d = p.diagrams.get(&diagram).ok_or(Refusal::StaleScope)?;
let root = p.elements.get(&work).ok_or(Refusal::StaleScope)?;
if !root.kind().is_work()
|| d.projection != ProjectionKind::Allocation
|| d.layer != root.layer()
{
return Err(Refusal::Ineligible(
"Select an activity or function in an Allocation view.".into(),
));
}
if d.nodes.get(&placement).is_none_or(|n| n.element != work) {
return Err(Refusal::StaleScope);
}
let mut work_records = BTreeMap::new();
let mut leaves = BTreeMap::new();
let mut pending = vec![work];
while let Some(id) = pending.pop() {
let element = p.elements.get(&id).ok_or(Refusal::StaleScope)?;
if element.kind() != root.kind() || work_records.insert(id, element.clone()).is_some() {
return Err(Refusal::StaleScope);
}
let children: Vec<_> = p
.children(id)
.filter(|e| e.kind() == root.kind())
.map(|e| e.id)
.collect();
if !children.is_empty() {
pending.extend(children);
continue;
}
let direct: Vec<_> = p
.relations
.values()
.filter(|r| r.kind() == RelationKind::Allocation && r.target() == id)
.cloned()
.collect();
if direct.len() > 1 {
return Err(Refusal::StaleScope);
}
let role_assignments: Vec<_> = p
.relations
.values()
.filter(|r| r.kind() == RelationKind::RoleAssignment && r.target() == id)
.cloned()
.collect();
let roles: BTreeSet<_> = role_assignments.iter().map(Relation::source).collect();
let role_realizations = p
.relations
.values()
.filter(|r| r.kind() == RelationKind::RoleRealization && roles.contains(&r.target()))
.cloned()
.collect();
leaves.insert(
id,
Leaf {
direct: direct.into_iter().next(),
role_assignments,
role_realizations,
effective_performer: validation::effective_performer(p, id),
},
);
}
if leaves.is_empty() {
return Err(Refusal::StaleScope);
}
let uniform = if leaves
.values()
.all(|l| l.direct.is_none() && l.role_assignments.is_empty())
{
Uniform::Unassigned
} else if let Some(performer) = leaves.values().next().and_then(|l| l.effective_performer)
&& leaves
.values()
.all(|l| l.effective_performer == Some(performer))
{
Uniform::Assigned(performer)
} else {
Uniform::MixedOrUnresolved
};
let placements = d
.nodes
.iter()
.filter(|(_, n)| work_records.contains_key(&n.element))
.map(|(id, n)| (*id, n.clone()))
.collect();
Ok(Scope {
project: p.id,
work,
placement,
work_records,
leaves,
placements,
diagram: d.clone(),
uniform,
})
}
#[derive(Debug)]
pub struct SemanticPlan {
pub scope: Scope,
pub destination: Destination,
pub label: String,
pub warnings: Vec<Issue>,
accepted: Project,
candidate: Project,
index: ViewIndex,
commands: Vec<Command>,
before_visible: ProjectedView,
before_expanded: ProjectedView,
}
impl SemanticPlan {
/// All preview drawing, ports and routing must use this candidate and index.
pub fn project(&self) -> &Project {
&self.candidate
}
pub fn index(&self) -> &ViewIndex {
&self.index
}
}
/// Allocation identities are allocated once by the gesture owner, keyed by leaf,
/// and reused on target revisits and release. No random identity generation here.
pub fn plan_semantics(
p: &Project,
scope: &Scope,
destination: Destination,
allocation_ids: &BTreeMap<Id, Id>,
) -> Result<SemanticPlan, Refusal> {
if &capture(p, scope.diagram.id, scope.work, scope.placement)? != scope {
return Err(Refusal::StaleScope);
}
if scope.work_records.len() > 1 && scope.uniform == Uniform::MixedOrUnresolved {
return Err(Refusal::MixedLayoutRequired);
}
if let Destination::Performer(id) = destination {
let valid = p
.elements
.get(&id)
.is_some_and(|e| e.kind().is_performer() && e.layer() == scope.diagram.layer);
if !valid {
return Err(Refusal::Ineligible(
"Choose a performer in this layer.".into(),
));
}
}
let before_index = ViewIndex::new(p);
let before_visible = projection::project_diagram_with_index(p, &scope.diagram, &before_index);
let before_expanded = expanded_view(p, &scope.diagram, scope, &before_index);
if scope.work_records.len() > 1 && !coherent(scope, &before_expanded) {
return Err(Refusal::MixedLayoutRequired);
}
let same_performer =
matches!(destination, Destination::Performer(id) if scope.uniform == Uniform::Assigned(id));
let mut expected = p.clone();
let mut commands = Vec::new();
let mut deleted = BTreeSet::new();
if destination != Destination::LayoutOnly && !same_performer {
for (work, leaf) in &scope.leaves {
match destination {
Destination::Performer(performer) => {
if let Some(existing) = &leaf.direct {
if existing.source() != performer {
let mut edited = existing.clone();
edited.body = RelationBody::Allocation {
source: performer,
target: *work,
};
expected.relations.insert(edited.id, edited.clone());
commands.push(Command::EditRelation(edited.id, edited));
}
} else {
let id = *allocation_ids
.get(work)
.ok_or(Refusal::MissingAllocationId(*work))?;
if id.is_nil() || expected.contains(id) {
return Err(Refusal::InvalidCandidate(
"The gesture's allocation identity is already in use.".into(),
));
}
let name = format!("{} performs {}", p.named(performer), p.named(*work));
let relation = Relation {
id,
alias: expected.fresh_alias(&name, "allocation", id),
name,
description: String::new(),
body: RelationBody::Allocation {
source: performer,
target: *work,
},
};
expected.relations.insert(id, relation.clone());
commands.push(Command::CreateRelation(relation));
}
}
Destination::OutsidePerformers => {
if !leaf.role_assignments.is_empty() {
let roles: Vec<_> = leaf
.role_assignments
.iter()
.map(Relation::source)
.collect::<BTreeSet<_>>()
.into_iter()
.collect();
let performers: Vec<_> = leaf
.role_realizations
.iter()
.map(Relation::source)
.collect::<BTreeSet<_>>()
.into_iter()
.collect();
let message = if leaf.direct.is_some() && performers.len() == 1 {
format!(
"Removing direct assignment would place {} in {} through its role.",
p.named(*work),
p.named(performers[0])
)
} else {
format!(
"{} retains role responsibility through {}{}; edit that responsibility before moving outside performers.",
p.named(*work),
roles
.iter()
.map(|id| p.named(*id))
.collect::<Vec<_>>()
.join(", "),
if performers.is_empty() {
String::new()
} else {
format!(
" ({})",
performers
.iter()
.map(|id| p.named(*id))
.collect::<Vec<_>>()
.join(", ")
)
}
)
};
return Err(Refusal::RoleExitRequired {
work: *work,
roles,
performers,
message,
});
}
if let Some(existing) = &leaf.direct {
expected.relations.remove(&existing.id);
deleted.insert(existing.id);
commands.push(Command::Delete(existing.id));
}
}
Destination::LayoutOnly => unreachable!(),
}
}
}
// A route cannot survive the intentional deletion of its own edge, even in
// another diagram. No other saved-view cleanup is authorized by this drag.
for diagram in expected.diagrams.values_mut() {
diagram.routes.retain(|r| !deleted.contains(&r.edge));
}
let actual = prepare_command(p, &Command::Batch(commands.clone()))
.map_err(|e| Refusal::InvalidCandidate(e.to_string()))?
.project;
exact_project(&expected, &actual)?;
let retained_roles = scope
.leaves
.values()
.any(|l| !l.role_assignments.is_empty());
let label = match destination {
Destination::LayoutOnly => "Arrange only".into(),
Destination::Performer(id) if same_performer => format!("Arrange within {}", p.named(id)),
Destination::Performer(id) => format!(
"Assign {} leaf {} to {}{}",
scope.leaves.len(),
match (scope.diagram.layer, scope.leaves.len()) {
(Layer::Oa, 1) => "activity",
(Layer::Oa, _) => "activities",
(Layer::Sa, 1) => "function",
(Layer::Sa, _) => "functions",
},
p.named(id),
if retained_roles {
" — explicit assignment; role responsibility retained"
} else {
""
}
),
Destination::OutsidePerformers if commands.is_empty() => {
"Arrange unassigned work — decomposition retained".into()
}
Destination::OutsidePerformers => "Remove assignment — decomposition retained".into(),
};
let warnings = validation::validate(&actual)
.into_iter()
.filter(|i| i.severity == Severity::Warning)
.collect();
let index = ViewIndex::new(&actual);
Ok(SemanticPlan {
scope: scope.clone(),
destination,
label,
warnings,
accepted: p.clone(),
candidate: actual,
index,
commands,
before_visible,
before_expanded,
})
}
/// World-space rectangle in the gesture's frozen transform. The UI resolver
/// owns half-open pointer hits, semantic target precedence and occlusion.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Rect {
pub position: Position,
pub size: Size,
}
impl Rect {
fn valid(self) -> bool {
[
self.position.x,
self.position.y,
self.size.width,
self.size.height,
]
.iter()
.all(|n| n.is_finite() && n.abs() <= MAX_COORDINATE)
&& self.size.width > 0.
&& self.size.height > 0.
}
fn contains(self, other: Self) -> bool {
other.position.x >= self.position.x
&& other.position.y >= self.position.y
&& other.position.x + other.size.width <= self.position.x + self.size.width
&& other.position.y + other.size.height <= self.position.y + self.size.height
}
fn overlaps(self, other: Self) -> bool {
self.position.x < other.position.x + other.size.width
&& other.position.x < self.position.x + self.size.width
&& self.position.y < other.position.y + other.size.height
&& other.position.y < self.position.y + self.size.height
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TargetPresentation {
pub placement: Id,
/// Exact frozen painted label bounds, supplied by the renderer/resolver.
pub label: Rect,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PresentationContext {
/// Visible world rectangle derived from the actual captured pan/zoom/bounds.
pub viewport: Rect,
pub zoom: f32,
pub target: Option<TargetPresentation>,
}
#[derive(Debug)]
pub struct PlacementPlan {
pub diagram: Diagram,
pub view: ProjectedView,
pub delta: Position,
pub unfolded: Option<Id>,
requested: Position,
presentation: PresentationContext,
commands: Vec<Command>,
}
/// `requested` is the original selected placement position plus pointer motion
/// in the frozen transform (therefore preserving the press offset). Snap only
/// this root; apply the resulting identical delta to captured Q and its routes.
/// No whole Project clone, validator or semantic ViewIndex rebuild occurs here.
pub fn place(
plan: &SemanticPlan,
requested: Position,
presentation: PresentationContext,
) -> Result<PlacementPlan, Refusal> {
if !requested.x.is_finite()
|| !requested.y.is_finite()
|| !presentation.viewport.valid()
|| !presentation.zoom.is_finite()
|| presentation.zoom <= 0.
{
return Err(Refusal::LayoutRequired(
"Make room or adjust the view, then drag again.".into(),
));
}
let root = &plan.scope.placements[&plan.scope.placement];
let delta = Position {
x: (requested.x / 10.).round() * 10. - root.position.x,
y: (requested.y / 10.).round() * 10. - root.position.y,
};
let mut diagram = plan.candidate.diagrams[&plan.scope.diagram.id].clone();
let mut commands = Vec::new();
let mut deltas = BTreeMap::new();
for (id, original) in &plan.scope.placements {
let node = diagram.nodes.get_mut(id).ok_or(Refusal::StaleScope)?;
node.position = Position {
x: original.position.x + delta.x,
y: original.position.y + delta.y,
};
if !(Rect {
position: node.position,
size: node.size,
})
.valid()
{
return Err(Refusal::InvalidCandidate(
"Moved coordinates exceed the model bounds.".into(),
));
}
deltas.insert(*id, delta);
if node != original {
commands.push(Command::EditNode {
diagram: diagram.id,
original: *id,
node: node.clone(),
});
}
}
// Resolve endpoints using the captured accepted semantics, not a newly
// regrouped descendant walk. Own-edge deletions were removed above already.
for route in projection::translated_routes(&plan.accepted, &plan.scope.diagram, &deltas) {
if let Some(existing) = diagram.routes.iter_mut().find(|r| r.edge == route.edge) {
if route.waypoints.iter().any(|p| {
!p.x.is_finite()
|| !p.y.is_finite()
|| p.x.abs() > MAX_COORDINATE
|| p.y.abs() > MAX_COORDINATE
}) {
return Err(Refusal::InvalidCandidate(
"Moved route exceeds the model bounds.".into(),
));
}
*existing = route.clone();
commands.push(Command::SetEdgeRoute {
diagram: diagram.id,
route,
});
}
}
let mut unfolded = None;
if let Destination::Performer(performer) = plan.destination {
let target = presentation.target.ok_or_else(|| {
Refusal::LayoutRequired("The frozen performer label must be visible.".into())
})?;
let node = diagram
.nodes
.get_mut(&target.placement)
.ok_or(Refusal::StaleScope)?;
let frozen_header = plan
.before_visible
.nodes
.iter()
.find(|n| n.id == target.placement)
.map(|n| Rect {
position: n.position,
size: Size {
width: n.size.width,
height: n.size.height.min(62.),
},
});
if node.element != performer
|| frozen_header.is_none_or(|header| !header.contains(target.label))
|| !target.label.valid()
|| !presentation.viewport.contains(target.label)
{
return Err(Refusal::LayoutRequired(
"Make room or adjust the view, then drag again.".into(),
));
}
if node.collapsed && !plan.commands.is_empty() {
node.collapsed = false;
unfolded = Some(node.id);
commands.push(Command::EditNode {
diagram: diagram.id,
original: node.id,
node: node.clone(),
});
}
} else if presentation.target.is_some() {
return Err(Refusal::Ineligible(
"This layout/outside destination has no performer target.".into(),
));
}
let view = projection::project_diagram_with_index(&plan.candidate, &diagram, &plan.index);
let expanded = expanded_view(&plan.candidate, &diagram, &plan.scope, &plan.index);
check_geometry(plan, &view, &expanded, delta)?;
let selected = view
.nodes
.iter()
.find(|n| n.id == plan.scope.placement)
.ok_or_else(|| {
Refusal::LayoutRequired("The selected work header would be hidden.".into())
})?;
let header = Rect {
position: selected.position,
size: Size {
width: selected.size.width,
height: selected.size.height.min(62.),
},
};
if !presentation.viewport.contains(header) {
return Err(Refusal::LayoutRequired(
"Make room or adjust the view, then drag again.".into(),
));
}
// A performer may legitimately enclose the work, but its frozen feedback
// label is painted above the candidate. Check their joint readability after
// projection, including same-performer moves; never shift either rectangle.
if presentation
.target
.is_some_and(|target| header.overlaps(target.label))
{
return Err(Refusal::LayoutRequired(
"The work header overlaps the performer label. Aim below or beside the label, then drag again."
.into(),
));
}
Ok(PlacementPlan {
diagram,
view,
delta,
unfolded,
requested,
presentation,
commands,
})
}
/// Final semantic/presentation equality gate, after the App has checked its
/// current token, owner, revision, clean drafts, frozen target and transform.
/// Recompute geometry to avoid accepting a modified/stale placement result.
/// None means a snapped no-op: the caller must not execute or add history.
pub fn finish(
plan: &SemanticPlan,
live: &Project,
placement: &PlacementPlan,
) -> Result<Option<Command>, Refusal> {
if live != &plan.accepted {
return Err(Refusal::StaleScope);
}
let checked = place(plan, placement.requested, placement.presentation)?;
if checked.diagram != placement.diagram
|| checked.view != placement.view
|| checked.delta != placement.delta
|| checked.unfolded != placement.unfolded
|| checked.commands != placement.commands
{
return Err(Refusal::StaleScope);
}
let mut expected = plan.candidate.clone();
expected
.diagrams
.insert(checked.diagram.id, checked.diagram);
let mut commands = plan.commands.clone();
commands.extend(checked.commands);
let command = Command::Batch(commands);
let actual = prepare_command(live, &command)
.map_err(|e| Refusal::InvalidCandidate(e.to_string()))?
.project;
exact_project(&expected, &actual)?;
Ok((actual != *live).then_some(command))
}
fn expanded_view(
p: &Project,
diagram: &Diagram,
scope: &Scope,
index: &ViewIndex,
) -> ProjectedView {
let mut expanded = diagram.clone();
for id in scope.placements.keys() {
if let Some(node) = expanded.nodes.get_mut(id) {
node.collapsed = false;
}
}
projection::project_diagram_with_index(p, &expanded, index)
}
fn ancestors(view: &ProjectedView) -> BTreeMap<Id, BTreeSet<Id>> {
let parents: BTreeMap<_, _> = view.nodes.iter().map(|n| (n.id, n.visual_parent)).collect();
parents
.keys()
.map(|node| {
let mut current = parents[node];
let mut seen = BTreeSet::new();
while let Some(id) = current {
if !seen.insert(id) {
break;
}
current = parents.get(&id).copied().flatten();
}
(*node, seen)
})
.collect()
}
fn coherent(scope: &Scope, view: &ProjectedView) -> bool {
let ancestors = ancestors(view);
scope.placements.keys().all(|id| {
ancestors
.get(id)
.is_some_and(|parents| *id == scope.placement || parents.contains(&scope.placement))
})
}
fn overlap(a: &ProjectedNode, b: &ProjectedNode) -> f32 {
let width = (a.position.x + a.size.width).min(b.position.x + b.size.width)
- a.position.x.max(b.position.x);
let height = (a.position.y + a.size.height).min(b.position.y + b.size.height)
- a.position.y.max(b.position.y);
width.max(0.) * height.max(0.)
}
fn overlaps(before: &ProjectedView, after: &ProjectedView) -> bool {
// Build geometry ancestry once, avoiding a repeated descendant walk for
// every pair during pointer motion. Pairwise collision checking is bounded
// to these two projected scenes and does not traverse/clone model semantics.
let before_ancestors = ancestors(before);
let after_ancestors = ancestors(after);
let prior_nodes: BTreeMap<_, _> = before.nodes.iter().map(|n| (n.id, n)).collect();
for (i, a) in after.nodes.iter().enumerate() {
for b in &after.nodes[i + 1..] {
if after_ancestors[&a.id].contains(&b.id) || after_ancestors[&b.id].contains(&a.id) {
continue;
}
let prior = match (prior_nodes.get(&a.id), prior_nodes.get(&b.id)) {
(Some(x), Some(y))
if !before_ancestors[&x.id].contains(&y.id)
&& !before_ancestors[&y.id].contains(&x.id) =>
{
overlap(x, y)
}
_ => 0.,
};
if overlap(a, b) > prior + 0.01 {
return true;
}
}
}
false
}
fn check_geometry(
plan: &SemanticPlan,
visible: &ProjectedView,
expanded: &ProjectedView,
delta: Position,
) -> Result<(), Refusal> {
if visible.nodes.iter().chain(&expanded.nodes).any(|n| {
!(Rect {
position: n.position,
size: n.size,
})
.valid()
}) {
return Err(Refusal::InvalidCandidate(
"The projected enclosure exceeds the model bounds.".into(),
));
}
if plan.scope.work_records.len() > 1 {
if !coherent(&plan.scope, expanded) {
return Err(Refusal::MixedLayoutRequired);
}
for id in plan.scope.placements.keys() {
let old = plan
.before_expanded
.nodes
.iter()
.find(|n| n.id == *id)
.ok_or(Refusal::MixedLayoutRequired)?;
let new = expanded
.nodes
.iter()
.find(|n| n.id == *id)
.ok_or(Refusal::MixedLayoutRequired)?;
if (new.position.x - old.position.x - delta.x).abs() > 0.01
|| (new.position.y - old.position.y - delta.y).abs() > 0.01
|| (new.size.width - old.size.width).abs() > 0.01
|| (new.size.height - old.size.height).abs() > 0.01
{
return Err(Refusal::LayoutRequired("This work subtree requires an explicit layout; its footprint cannot be translated unchanged.".into()));
}
}
}
if overlaps(&plan.before_visible, visible) || overlaps(&plan.before_expanded, expanded) {
return Err(Refusal::LayoutRequired("Make room before moving work: the candidate would introduce or enlarge overlapping cards.".into()));
}
Ok(())
}
/// Compare complete records, not just removed IDs. Deleting an Allocation can
/// rewrite a surviving Constraint, remove artifacts/traces or prune view options.
fn exact_project(expected: &Project, actual: &Project) -> Result<(), Refusal> {
if expected == actual {
return Ok(());
}
let mut records = Vec::new();
fn changes<T: PartialEq>(
expected: &BTreeMap<Id, T>,
actual: &BTreeMap<Id, T>,
category: &'static str,
project: &Project,
other: &Project,
records: &mut Vec<ChangedRecord>,
) {
let ids: BTreeSet<_> = expected.keys().chain(actual.keys()).copied().collect();
for id in ids {
if expected.get(&id) != actual.get(&id) {
records.push(ChangedRecord {
id,
name: if project.contains(id) {
project.named(id)
} else {
other.named(id)
}
.into(),
category,
});
}
}
}
changes(
&expected.elements,
&actual.elements,
"element",
expected,
actual,
&mut records,
);
changes(
&expected.relations,
&actual.relations,
"relation",
expected,
actual,
&mut records,
);
changes(
&expected.diagrams,
&actual.diagrams,
"diagram",
expected,
actual,
&mut records,
);
changes(
&expected.artifact_links,
&actual.artifact_links,
"artifact",
expected,
actual,
&mut records,
);
changes(
&expected.traces,
&actual.traces,
"trace",
expected,
actual,
&mut records,
);
if expected.schema_version != actual.schema_version
|| expected.id != actual.id
|| expected.alias != actual.alias
|| expected.name != actual.name
|| expected.description != actual.description
{
records.push(ChangedRecord {
id: expected.id,
name: expected.name.clone(),
category: "project",
});
}
Err(Refusal::CollateralChangeRequired(records))
}
#[cfg(test)]
mod tests;
File diff suppressed because it is too large Load Diff
+266
View File
@@ -0,0 +1,266 @@
//! A narrow dispatch boundary for Allocation work gestures. Iced rows visit
//! siblings after capture; refusing inside Canvas alone still blurs an inspector
//! field. Refuse before dispatching that press to any ordinary child instead.
use crate::{
app::{
App, Message,
allocation_drag::{self, Event as WorkEvent},
},
diagram::DiagramCanvas,
};
use iced::advanced::{
Clipboard, Layout, Shell, Widget, layout, overlay, renderer,
widget::{Id, Operation, Tree, tree},
};
use iced::{
Element, Event, Length, Point, Rectangle, Renderer, Size, Theme, Vector, keyboard, mouse,
};
pub const CANVAS_ID: &str = "allocation-work-canvas";
pub const ARRANGE_SHORTCUT: &str = "Alt+A";
struct Root<'a> {
app: &'a App,
content: Element<'a, Message>,
}
#[derive(Default)]
struct State {
pointer: Option<Point>,
refused_left: bool,
}
#[derive(Default)]
struct CanvasBounds(Option<Rectangle>);
impl Operation for CanvasBounds {
fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn Operation)) {
visit(self);
}
fn container(&mut self, id: Option<&Id>, bounds: Rectangle) {
if id == Some(&Id::new(CANVAS_ID)) {
self.0 = Some(bounds);
}
}
}
pub fn root<'a>(app: &'a App, content: impl Into<Element<'a, Message>>) -> Element<'a, Message> {
Element::new(Root {
app,
content: content.into(),
})
}
fn arrange_key(event: &Event) -> bool {
matches!(event, Event::Keyboard(keyboard::Event::KeyPressed {
key: keyboard::Key::Character(key), modifiers, repeat: false, ..
}) if key.eq_ignore_ascii_case("a") && *modifiers == keyboard::Modifiers::ALT)
}
impl Root<'_> {
fn dirty_object(&self, bounds: Rectangle, screen: Point) -> bool {
let Some(diagram) = self.app.active_diagram else {
return false;
};
if !allocation_drag::contains(bounds, screen) {
return false;
}
let canvas = DiagramCanvas {
allocation: Some(self.app.allocation_capability()),
project: self.app.project(),
view_index: Some(self.app.document.view_index()),
diagram,
selected: self.app.selected,
zoom: self.app.zoom,
pan: self.app.pan,
context: self.app.route_context().unwrap(),
selected_bend: None,
editable: false,
placing: false,
connection: None,
};
canvas.work_dirty_point(bounds, screen)
}
}
impl Widget<Message, Theme, Renderer> for Root<'_> {
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State>()
}
fn state(&self) -> tree::State {
tree::State::new(State::default())
}
fn children(&self) -> Vec<Tree> {
vec![Tree::new(self.content.as_widget())]
}
fn diff(&self, tree: &mut Tree) {
tree.diff_children(std::slice::from_ref(&self.content));
}
fn size(&self) -> Size<Length> {
self.content.as_widget().size()
}
fn layout(
&mut self,
tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
self.content
.as_widget_mut()
.layout(&mut tree.children[0], renderer, limits)
}
fn operate(
&mut self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn Operation,
) {
self.content
.as_widget_mut()
.operate(&mut tree.children[0], layout, renderer, operation);
}
fn update(
&mut self,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) {
let state = tree.state.downcast_mut::<State>();
match event {
Event::Mouse(mouse::Event::CursorMoved { position }) => state.pointer = Some(*position),
Event::Mouse(mouse::Event::CursorLeft) => state.pointer = None,
Event::Window(iced::window::Event::RedrawRequested(_)) => {
state.pointer = cursor.position()
}
Event::Window(iced::window::Event::Unfocused) => {
state.pointer = None;
state.refused_left = false;
}
_ => {}
}
if state.refused_left {
if matches!(
event,
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
) {
state.refused_left = false;
shell.capture_event();
return;
}
if matches!(event, Event::Mouse(mouse::Event::CursorMoved { .. })) {
shell.capture_event();
return;
}
}
if self.app.allocation_input_owner() && !shell.is_event_captured() {
if arrange_key(event) {
// App rechecks clean ownership before arming. A dirty field
// retains its actual widget focus and selection on refusal.
shell.publish(Message::WorkDrag(WorkEvent::ArrangeNext));
shell.capture_event();
return;
}
if self.app.allocation_drag.arrange_next
&& matches!(
event,
Event::Keyboard(keyboard::Event::KeyPressed {
key: keyboard::Key::Named(keyboard::key::Named::Escape),
..
})
)
{
shell.publish(Message::Escape);
shell.capture_event();
return;
}
if !self.app.allocation_drag_clean()
&& matches!(
event,
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
)
&& let Some(screen) = state.pointer.or_else(|| cursor.position())
{
let mut canvas = CanvasBounds::default();
self.content.as_widget_mut().operate(
&mut tree.children[0],
layout,
renderer,
&mut canvas,
);
if canvas
.0
.is_some_and(|bounds| self.dirty_object(bounds, screen))
{
state.refused_left = true;
shell.publish(Message::WorkDrag(WorkEvent::Refused(
self.app.allocation_drag.epoch,
allocation_drag::FINISH_EDIT.into(),
)));
shell.capture_event();
return;
}
}
}
self.content.as_widget_mut().update(
&mut tree.children[0],
event,
layout,
cursor,
renderer,
clipboard,
shell,
viewport,
);
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
) {
self.content.as_widget().draw(
&tree.children[0],
renderer,
theme,
style,
layout,
cursor,
viewport,
);
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
self.content.as_widget().mouse_interaction(
&tree.children[0],
layout,
cursor,
viewport,
renderer,
)
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'b>,
renderer: &Renderer,
viewport: &Rectangle,
translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
self.content.as_widget_mut().overlay(
&mut tree.children[0],
layout,
renderer,
viewport,
translation,
)
}
}
+330 -41
View File
@@ -1,10 +1,15 @@
pub(crate) mod allocation_drag;
pub(crate) mod architecture_authoring;
#[cfg(test)]
mod architecture_authoring_tests;
mod checkpoints;
pub(crate) mod diagram_removal;
#[cfg(test)]
mod diagram_removal_tests;
#[cfg(test)]
mod diagram_selection_tests;
mod hosted;
mod reference_authoring;
#[cfg(test)]
mod review_navigation_tests;
#[cfg(test)]
@@ -57,7 +62,7 @@ mod direct_scenario_tests {
}
}
#[test]
fn direct_scenario_scroll_and_real_resize_invalidate_only_pointer_context() {
fn direct_scenario_camera_details_and_real_resize_invalidate_only_pointer_context() {
let (mut app, f) = setup();
let original = app.document.bundle().clone();
let event = begin(&app, &f);
@@ -73,18 +78,26 @@ mod direct_scenario_tests {
let _ = app.update(Message::ScenarioCanvas(event));
assert!(!app.behavior.has_pending());
let context = app.scenario_context().unwrap();
for (area, offset) in [
(scenario::ScrollArea::Canvas, iced::Vector::new(30., 150.)),
(scenario::ScrollArea::Page, iced::Vector::new(0., 90.)),
] {
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Camera(
context,
app.camera_epoch,
behavior::canvas_workspace::Event::Pan(iced::Vector::new(30., 150.)),
)));
assert_ne!(app.scenario_context(), Some(context));
let context = app.scenario_context().unwrap();
for offset in [iced::Vector::new(0., 90.), iced::Vector::new(0., 150.)] {
let before = app.scenario_context().unwrap();
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Scrolled(
context, area, offset,
context,
scenario::ScrollArea::Page,
offset,
)));
assert_ne!(app.scenario_context(), Some(before));
let current = app.scenario_context().unwrap();
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Scrolled(
context, area, offset,
context,
scenario::ScrollArea::Page,
offset,
)));
assert_eq!(
app.scenario_context(),
@@ -113,7 +126,7 @@ mod direct_scenario_tests {
}
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Scrolled(
stale,
scenario::ScrollArea::Canvas,
scenario::ScrollArea::Page,
iced::Vector::new(0., 900.),
)));
assert_eq!(app.scenario_context(), Some(current));
@@ -318,6 +331,8 @@ pub struct FilePrompt {
#[derive(Debug, Clone)]
pub enum Message {
WorkDrag(allocation_drag::Event),
BehaviorControl(behavior::canvas_workspace::Intent),
Architecture(architecture_authoring::Event),
Home(crate::home::Event),
CheckpointTick,
@@ -371,6 +386,8 @@ pub enum Message {
FieldValuePath(String, crate::data_values::FieldPathKey),
FieldValueBrowse(String, Option<crate::data_values::FieldPathKey>, usize),
FieldReferenceBrowse(String, String, usize),
FieldReferenceOpen(crate::forms::reference_chooser::adapter::Open),
FieldReferenceEvent(crate::forms::reference_chooser::Event),
FieldVariant(String, Value),
FieldArray(String, Option<usize>, i32),
Apply,
@@ -379,7 +396,9 @@ pub enum Message {
RelationKind(RelationKind),
Command(Command),
Delete,
ConfirmDelete,
ConfirmDelete(u64),
DiagramRemove(diagram_removal::Intent),
DiagramTraverse(bool),
CancelDelete,
Undo,
Redo,
@@ -438,6 +457,7 @@ pub enum Message {
}
pub struct App {
pub allocation_drag: allocation_drag::State,
pub home: crate::home::State,
pub recovery: crate::recovery::State,
recent_target: Option<PathBuf>,
@@ -445,10 +465,14 @@ pub struct App {
pub hosted: crate::hosted::State,
pub connection: Option<crate::connection_edit::Session>,
pub model_menu: Option<crate::model_menu::Opened>,
reference_authoring: reference_authoring::State,
pub source_tree: crate::source_tree::State,
menu_sequence: u64,
pub canvas_document: Id,
pub canvas_epoch: u64,
/// Camera batches survive their own updates, never external presentation changes.
pub camera_epoch: u64,
camera_boundary_epoch: u64,
pub selected_bend: Option<(Id, usize)>,
pub exact_strings: bool,
pub document: Document,
@@ -473,6 +497,7 @@ pub struct App {
pub busy: bool,
pub pending: Option<Pending>,
pub delete_pending: bool,
pub delete_target: Option<diagram_removal::Confirmation>,
pub path: Option<PathBuf>,
/// A disk transaction requiring an explicit action, independent of lint.
pub pending_recovery: Option<PathBuf>,
@@ -541,16 +566,20 @@ impl App {
let explorer_index = crate::explorer::Index::new(&project, layer);
let explorer_collapsed = project.elements.values().filter_map(|e| e.parent).collect();
let mut app = Self {
allocation_drag: Default::default(),
home: Default::default(),
recovery: Default::default(),
recent_target: None,
checkpoint_close: None,
connection: None,
model_menu: None,
reference_authoring: Default::default(),
source_tree: Default::default(),
menu_sequence: 0,
canvas_document: Id::new_v4(),
canvas_epoch: 0,
camera_epoch: 0,
camera_boundary_epoch: 0,
selected_bend: None,
exact_strings: false,
document,
@@ -579,6 +608,7 @@ impl App {
busy: false,
pending: None,
delete_pending: false,
delete_target: None,
path: None,
pending_recovery: None,
disk_project: None,
@@ -854,8 +884,12 @@ impl App {
let scenario_selection = self
.behavior
.scenario
.created_selection(&command)
.or_else(|| self.behavior.topology.created_selection(&command));
.created_selection(self.project(), &command)
.or_else(|| {
self.behavior
.topology
.created_selection(self.project(), &command)
});
match self.document.execute(command) {
Ok(selected) => {
if let Some(created) = created {
@@ -1023,7 +1057,13 @@ impl App {
crate::model_menu::popup(self),
)
.into();
crate::hosted::view::overlay(self, base)
crate::behavior::scenario::focus::root(
behavior::canvas_workspace::focus_root(
crate::hosted::view::overlay(self, self.reference_overlay(base)),
behavior::canvas_workspace::Scope::input_owner(self),
),
self.scenario_focus_owner(),
)
}
pub fn subscription(&self) -> Subscription<Message> {
Subscription::batch([
@@ -1031,9 +1071,21 @@ impl App {
window::close_requests().map(|id| Message::Request(Pending::Close(id))),
window::resize_events().map(|(_, size)| Message::WindowResized(size)),
architecture_authoring::subscription(self),
behavior::creation::subscription(self.behavior.creation.has_pending())
.map(|()| Message::Escape),
iced::event::listen_with(|event, _, _| {
matches!(
event,
iced::Event::Keyboard(keyboard::Event::KeyPressed { .. })
)
.then_some(Message::WorkDrag(allocation_drag::Event::KeyboardBoundary))
}),
behavior::creation::subscription(
self.behavior.creation.has_pending() || self.behavior.source_active(),
)
.map(|()| Message::Escape),
iced::event::listen_with(|event, status, _| {
if matches!(event, iced::Event::Window(window::Event::Unfocused)) {
return Some(Message::WorkDrag(allocation_drag::Event::Unfocused));
}
let iced::Event::Keyboard(keyboard::Event::KeyPressed {
key,
modifiers: mods,
@@ -1077,6 +1129,116 @@ impl App {
])
}
pub fn update(&mut self, message: Message) -> Task<Message> {
self.allocation_before(&message);
if let Message::WorkDrag(event) = message {
return self.allocation_event(event);
}
let allocation_before = self.allocation_boundary();
if let Message::BehaviorControl(intent) = message {
return self.behavior_control(intent);
}
let camera = matches!(
&message,
Message::ScenarioCanvas(crate::behavior::scenario::CanvasEvent::Camera(..))
| Message::TopologyCanvas(crate::behavior::topology::CanvasEvent::Camera(..))
);
let before = self.camera_boundary();
let task = self.update_inner(message);
if self.allocation_boundary() != allocation_before {
self.allocation_retire();
}
if !camera && self.camera_boundary() != before {
self.camera_epoch = self.camera_epoch.wrapping_add(1);
self.camera_boundary_epoch = self.canvas_epoch;
}
task
}
fn behavior_control(&mut self, intent: behavior::canvas_workspace::Intent) -> Task<Message> {
use behavior::canvas_workspace::{self, Event as Camera, Scope};
use behavior::scenario::CanvasEvent as Scenario;
use behavior::topology::CanvasEvent as Process;
if Scope::input_owner(self) != Some(intent.scope) {
return Task::none();
}
let mut message = *intent.message;
let scope = intent.scope;
let valid_scenario = |c: behavior::scenario::Context| {
c.document == scope.document
&& c.scenario == scope.owner
&& c.revision == scope.revision
&& self.camera_toolbar_epoch(c.epoch)
};
let valid_process = |c: behavior::topology::Context| {
c.document == scope.document
&& c.owner == scope.owner
&& c.revision == scope.revision
&& self.camera_toolbar_epoch(c.epoch)
};
// This whitelist contains only the production presentation controls.
// A stamped toolbar cannot smuggle an arbitrary authoring command.
match &mut message {
Message::ScenarioCanvas(Scenario::Camera(c, lifetime, Camera::Focus))
if valid_scenario(*c) && *lifetime == scope.epoch =>
{
return canvas_workspace::focus_task(scope, Camera::Focus);
}
Message::TopologyCanvas(Process::Camera(c, lifetime, Camera::Focus))
if valid_process(*c) && *lifetime == scope.epoch =>
{
return canvas_workspace::focus_task(scope, Camera::Focus);
}
Message::ScenarioCanvas(
Scenario::Zoom(c, _)
| Scenario::Choose(c, _)
| Scenario::Focus(c)
| Scenario::Shelf(c, _),
) if valid_scenario(*c) => {
c.epoch = self.canvas_epoch;
}
Message::TopologyCanvas(
Process::Zoom(c, _) | Process::Choose(c, _) | Process::Focus(c),
) if valid_process(*c) => {
c.epoch = self.canvas_epoch;
}
Message::Behavior(behavior::Event::Topology(behavior::topology::Event::Shelf(_)))
if self.topology_context().is_some() => {}
_ => return Task::none(),
}
let task = self.update(message);
// Reapplying Fit or a tool also ends a held gesture. Focus is stamped
// after the action, for Iced's final-view ordering.
if self.camera_epoch == scope.epoch {
self.camera_epoch = self.camera_epoch.wrapping_add(1);
self.camera_boundary_epoch = self.canvas_epoch;
}
let focus = Scope::input_owner(self).map_or_else(Task::none, |scope| {
canvas_workspace::focus_task(scope, Camera::ControlFocus(intent.name))
});
Task::batch([task, focus])
}
fn camera_toolbar_epoch(&self, epoch: u64) -> bool {
epoch >= self.camera_boundary_epoch && epoch <= self.canvas_epoch
}
fn camera_boundary(&self) -> impl PartialEq + use<> {
(
self.canvas_epoch,
self.canvas_document,
self.document.revision(),
crate::behavior::selected_owner(self),
self.page,
self.window_size,
crate::behavior::canvas_workspace::keyboard_enabled(self),
self.behavior.has_pending(),
self.draft_dirty(),
self.source_dirty(),
self.behavior.topology.camera_boundary(),
self.behavior.scenario.camera_boundary(),
)
}
fn update_inner(&mut self, message: Message) -> Task<Message> {
if let Some(task) = self.reference_message(&message) {
return task;
}
if matches!(&message, Message::Connection(_))
&& self
.architecture
@@ -1211,6 +1373,8 @@ impl App {
}
if self.delete_pending {
self.delete_pending = false;
self.delete_target = None;
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
return Task::none();
}
if self.pending.take().is_some() {
@@ -1245,7 +1409,7 @@ impl App {
Message::FilePath(_) | Message::SubmitFile | Message::CancelFile
))
|| (self.delete_pending
&& !matches!(message, Message::ConfirmDelete | Message::CancelDelete))
&& !matches!(message, Message::ConfirmDelete(_) | Message::CancelDelete))
|| (self.pending.is_some()
&& self.file_prompt.is_none()
&& !matches!(
@@ -1299,6 +1463,7 @@ impl App {
self.last_workspace_page = self.page;
}
let mut task = self.handle(message);
self.reconcile_reference_chooser();
self.route_state_diagram_workspace();
if self.page != Page::Home {
self.last_workspace_page = self.page;
@@ -1366,8 +1531,24 @@ impl App {
epoch: self.canvas_epoch,
})
}
pub(crate) fn scenario_focus_owner(&self) -> Option<crate::behavior::scenario::focus::Stamp> {
if self.busy
|| self.hosted.visible
|| self.workflow.is_some()
|| self.architecture.is_some()
|| self.model_menu.is_some()
|| self.file_prompt.is_some()
|| self.delete_pending
|| self.pending.is_some()
{
return None;
}
self.behavior.scenario.focus_stamp(self.scenario_context()?)
}
fn handle(&mut self, message: Message) -> Task<Message> {
match message {
Message::WorkDrag(event) => return self.allocation_event(event),
Message::BehaviorControl(intent) => return self.behavior_control(intent),
Message::Architecture(event) => return self.architecture_event(event),
Message::Home(event) => return self.home_event(event),
Message::CheckpointTick => return self.checkpoint_tick(),
@@ -1383,6 +1564,18 @@ impl App {
Message::ScenarioCanvas(event) => {
use crate::behavior::scenario::{self, CanvasEvent};
let context = event.context();
if let CanvasEvent::FormShown(_, mount) = event {
if self.scenario_context().is_some_and(|current| {
current.document == context.document
&& current.scenario == context.scenario
&& current.revision == context.revision
}) && self.scenario_focus_owner().is_some()
&& self.behavior.scenario.take_form_focus(mount)
{
return scenario::focus::task(self.scenario_focus_owner().unwrap());
}
return Task::none();
}
if let CanvasEvent::Viewport(_, size) = event {
if self.scenario_context().is_some_and(|current| {
current.document == context.document
@@ -1407,9 +1600,9 @@ impl App {
return Task::none();
}
if let CanvasEvent::Scrolled(_, area, offset) = event {
// Canvas and outline notifications may come from the
// same frame. Accept both across the first epoch bump,
// while still rejecting another owner/document/revision.
// Details may report more than once in a frame. Accept the
// current owner's passive clamps across an epoch bump while
// still rejecting another document, owner or revision.
if self.scenario_context().is_some_and(|current| {
current.document == context.document
&& current.scenario == context.scenario
@@ -1420,6 +1613,49 @@ impl App {
}
return Task::none();
}
if let CanvasEvent::Camera(_, lifetime, input) = event {
use crate::behavior::canvas_workspace::{self, Event};
if lifetime != self.camera_epoch
|| !self.scenario_context().is_some_and(|c| {
c.document == context.document
&& c.scenario == context.scenario
&& c.revision == context.revision
})
{
return Task::none();
}
let scope = canvas_workspace::Scope {
document: context.document,
owner: context.scenario,
revision: context.revision,
epoch: lifetime,
};
if self.behavior.has_pending()
|| self.draft_dirty()
|| self.source_dirty()
|| !canvas_workspace::keyboard_enabled(self)
{
return Task::none();
}
match input {
Event::Focus
| Event::Traverse(_)
| Event::ControlFocus(_)
| Event::ToggleFocus
| Event::Leave => {
return canvas_workspace::focus_task(scope, input);
}
Event::CancelGesture => {
self.canvas_epoch = self.canvas_epoch.wrapping_add(1)
}
input => {
if self.behavior.scenario.camera(input) {
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
}
}
}
return Task::none();
}
if self.scenario_context() != Some(context) {
return Task::none();
}
@@ -1433,9 +1669,11 @@ impl App {
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
}
CanvasEvent::Zoom(_, scale) => {
if self.behavior.scenario.zoom(scale) {
let mut state = std::mem::take(&mut self.behavior.scenario);
if state.camera_zoom(self.project(), context.scenario, scale) {
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
}
self.behavior.scenario = state;
}
CanvasEvent::OpenUse(_, occurrence) => {
if self.behavior.has_pending() || self.draft_dirty() || self.source_dirty()
@@ -1526,13 +1764,22 @@ impl App {
..
} => {
let mut state = std::mem::take(&mut self.behavior.scenario);
let result = state.begin_occurrence(
self.project(),
context,
tool,
slot,
lifeline,
);
let result = if tool == scenario::Tool::Lifeline {
state
.update(
self.project(),
scenario::Event::Start(context.scenario, tool),
)
.map(|_| ())
} else {
state.begin_occurrence(
self.project(),
context,
tool,
slot,
lifeline,
)
};
self.behavior.scenario = state;
match result {
Ok(()) => {
@@ -1754,7 +2001,15 @@ impl App {
return self.update(Message::Add(child));
}
Message::Escape => {
if self.behavior.creation.has_pending() {
if self.behavior.source_back() {
self.status =
"Returned to the unchanged parent form; no source was created.".into();
return Task::none();
}
if self.behavior.source_active() {
self.behavior.acknowledge_commit();
self.status = "Behavior form cancelled; no staged source was created.".into();
} else if self.behavior.creation.has_pending() {
self.behavior.creation.acknowledge_commit();
self.status = "Behavior creation cancelled".into();
}
@@ -1817,6 +2072,9 @@ impl App {
.insert(path, crate::forms::ValueBrowser { scope, page });
}
}
Message::FieldReferenceOpen(_) | Message::FieldReferenceEvent(_) => {
return Task::none();
}
Message::FieldReferenceBrowse(path, query, page) => {
if let Some(draft) = &mut self.draft {
let page = if draft
@@ -2057,7 +2315,7 @@ impl App {
if let crate::workflow::Workflow::Semantic(state) = &mut workflow {
match event {
crate::workflow::Event::Semantic(event) => {
state.update(self.project(), event)
state.update(self.project(), *event)
}
crate::workflow::Event::Apply => {
let focus = state.target(self.project());
@@ -2321,10 +2579,13 @@ impl App {
self.status = "Apply or cancel the current draft before starting a new behavior. Your inputs are retained.".into();
return Task::none();
}
if !creating && event.starts_form() && !self.behavior.has_pending() && !self.apply()
if (event.starts_form() || (event.is_source() && !event.is_source_back()))
&& (self.source_dirty() || self.draft_dirty())
{
self.status="Apply or cancel the other editors before starting this form. Your inputs are retained.".into();
return Task::none();
}
let source_was_open = self.behavior.source_open();
if event.is_submit() && (self.source_dirty() || self.draft_dirty()) {
self.status = "Revert the other drafts before applying the behavior form. Your inputs are retained.".into();
return Task::none();
@@ -2342,6 +2603,11 @@ impl App {
}
}
Ok(None) => {
if !source_was_open && self.behavior.source_open() {
return iced::widget::operation::focus(
behavior::source_draft::NAME_INPUT,
);
}
if focus_name {
return iced::widget::operation::focus(behavior::creation::NAME_INPUT);
}
@@ -2521,29 +2787,41 @@ impl App {
}
}
}
Message::DiagramRemove(intent) => self.remove_from_diagram(intent),
Message::DiagramTraverse(back) => {
return if back {
iced::widget::operation::focus_previous()
} else {
iced::widget::operation::focus_next()
};
}
Message::Delete => {
if self.selected != self.project().id && self.apply() {
self.delete_pending = true;
self.begin_model_delete(None);
}
}
Message::ConfirmDelete => {
if !self.delete_pending || self.behavior.has_pending() {
return Task::none();
}
Message::ConfirmDelete(token) => self.confirm_model_delete(token),
Message::CancelDelete => {
self.delete_pending = false;
self.execute(Command::Delete(self.selected));
self.delete_target = None;
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
}
Message::CancelDelete => self.delete_pending = false,
Message::Undo | Message::Redo => {
let redo = matches!(message, Message::Redo);
if self.apply() {
if redo {
self.document.redo();
let changed = if redo {
self.document.redo()
} else {
self.document.undo();
self.document.undo()
};
if changed {
self.sync();
}
self.sync();
self.status = if redo {
self.status = if !changed && redo {
"Nothing to redo"
} else if !changed {
"Nothing to undo"
} else if redo {
"Model change redone"
} else {
"Model change undone"
@@ -2797,11 +3075,17 @@ impl App {
|| self.path.as_ref() == Some(&receipt.target)
{
self.path = Some(receipt.target);
self.document.bind_repair_baseline(disk.reachable_bundle());
self.disk_project = Some(disk);
}
self.status = "Interrupted disk save recovered. Current model and drafts are retained; Save all can be retried.".into();
}
Err(error) => {
if self.path.is_none()
|| self.path.as_ref() == Some(&receipt.target)
{
self.document.invalidate_saved_baseline();
}
self.status = format!(
"Disk recovery completed; files could not be reloaded: {error}"
)
@@ -3578,6 +3862,7 @@ impl App {
self.canvas_document = Id::new_v4();
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
self.path = Some(path);
self.document.bind_repair_baseline(disk.reachable_bundle());
self.source_workspace = Workspace::new(&disk.bundle, false);
self.source_workspace.reachable = disk.reachable.clone();
self.show_source_file(false);
@@ -4245,6 +4530,10 @@ fn read_snapshot(path: PathBuf) -> Result<LoadedSnapshot, String> {
#[path = "app/save_as_tests.rs"]
mod save_as_tests;
#[cfg(test)]
#[path = "app/repair_save_state_tests.rs"]
mod repair_save_state_tests;
#[cfg(test)]
mod source_project_tests {
use super::*;
@@ -0,0 +1,601 @@
//! Allocation work-body input admission. Gesture authority lives here, while
//! the canvas retains ordered native pointer positions and frozen hit geometry.
use super::{App, Message, Page};
use crate::{allocation_drag as plan, route_edit::Context};
use iced::{Point, Rectangle, Task, Vector};
use oa_core::{projection::ProjectedView, routing::RoutedView, *};
use std::{collections::BTreeMap, sync::Arc};
pub const FINISH_EDIT: &str = "Finish or cancel the current edit before selecting or moving work.";
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Stamp {
pub context: Context,
pub epoch: u64,
pub snapshot: usize,
pub bounds: Rectangle,
pub pan: Vector,
pub zoom: f32,
}
impl Stamp {
pub fn world(self, screen: Point) -> Point {
Point::new(
(screen.x - self.bounds.x - self.pan.x) / self.zoom,
(screen.y - self.bounds.y - self.pan.y) / self.zoom,
)
}
pub fn viewport(self) -> plan::Rect {
let p = self.world(Point::new(self.bounds.x, self.bounds.y));
plan::Rect {
position: Position { x: p.x, y: p.y },
size: Size {
width: self.bounds.width / self.zoom,
height: self.bounds.height / self.zoom,
},
}
}
}
#[derive(Debug, Clone)]
pub struct Target {
pub element: Id,
pub placement: Id,
pub region: Rectangle,
pub label: plan::Rect,
pub name: String,
pub ancestors: Vec<Id>,
}
#[derive(Debug, Clone)]
pub struct Frozen {
pub targets: Vec<Target>,
cards: Vec<Rectangle>,
controls: Vec<Rectangle>,
edges: Vec<(Point, Point)>,
radius: f32,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Hit {
Destination(plan::Destination, Option<usize>),
OutsideViewport,
Occluded,
Ambiguous(Vec<usize>),
}
pub fn contains(r: Rectangle, p: Point) -> bool {
p.x >= r.x && p.y >= r.y && p.x < r.x + r.width && p.y < r.y + r.height
}
impl Frozen {
pub fn capture(
project: &Project,
scope: &plan::Scope,
view: &ProjectedView,
routed: &RoutedView,
zoom: f32,
) -> Self {
let mut targets = Vec::new();
let mut cards = Vec::new();
for n in &view.nodes {
if scope.placements.contains_key(&n.id) {
continue;
}
let region = Rectangle::new(
Point::new(n.position.x, n.position.y),
iced::Size::new(n.size.width, n.size.height),
);
let Some(e) = project.elements.get(&n.element) else {
cards.push(region);
continue;
};
if e.kind().is_performer() && e.layer() == scope.diagram.layer {
let mut ancestors = Vec::new();
let mut parent = e.parent;
while let Some(id) = parent {
if ancestors.contains(&id) {
break;
}
ancestors.push(id);
parent = project.elements.get(&id).and_then(|e| e.parent);
}
// This is also the exact frozen label painted above the ghost.
let name = crate::views::short(
project.named(e.id),
(((region.width - 28.).max(10.) / 10.) as usize).min(34),
);
let label = plan::Rect {
position: Position {
x: region.x + 10.,
y: region.y + 31.,
},
size: Size {
width: (name.chars().count() as f32 * 10. + 8.)
.min((region.width - 20.).max(1.)),
height: 22.,
},
};
let collapsed = scope.diagram.nodes[&n.id].collapsed;
targets.push(Target {
element: e.id,
placement: n.id,
region: if collapsed {
Rectangle {
height: region.height.min(62.),
..region
}
} else {
region
},
label,
name,
ancestors,
});
} else {
cards.push(region);
}
}
let radius = (10. / zoom).max(10.);
let mut controls: Vec<_> = view
.ports
.iter()
.filter(|port| !scope.placements.contains_key(&port.owner))
.map(|port| {
Rectangle::new(
Point::new(port.position.x - radius, port.position.y - radius),
iced::Size::new(radius * 2., radius * 2.),
)
})
.collect();
let mut edges = Vec::new();
for edge in routed.edges.values() {
if let Some(relation) = project.relations.get(&edge.id) {
for end in routing::association_end_geometry(relation, edge) {
if !end.label.text.is_empty() {
controls.push(Rectangle::new(
Point::new(end.label.bounds.position.x, end.label.bounds.position.y),
iced::Size::new(
end.label.bounds.size.width,
end.label.bounds.size.height,
),
));
}
}
}
if !edge.label.text.is_empty() {
controls.push(Rectangle::new(
Point::new(edge.label.bounds.position.x, edge.label.bounds.position.y),
iced::Size::new(edge.label.bounds.size.width, edge.label.bounds.size.height),
));
}
for pin in &edge.pins {
controls.push(Rectangle::new(
Point::new(pin.x - 8. / zoom, pin.y - 8. / zoom),
iced::Size::new(16. / zoom, 16. / zoom),
));
}
for points in edge.points.windows(2) {
edges.push((
Point::new(points[0].x, points[0].y),
Point::new(points[1].x, points[1].y),
));
}
}
Self {
targets,
cards,
controls,
edges,
radius: 6. / zoom,
}
}
pub fn resolve(&self, stamp: Stamp, screen: Option<Point>, arrange: bool) -> Hit {
let Some(screen) = screen.filter(|p| contains(stamp.bounds, *p)) else {
return Hit::OutsideViewport;
};
let p = stamp.world(screen);
// Drag status lives in the toolbar, outside this actual canvas viewport.
if self
.controls
.iter()
.chain(&self.cards)
.any(|r| contains(*r, p))
|| self.edges.iter().any(|(a, b)| {
let d = *b - *a;
let length = d.x * d.x + d.y * d.y;
let t = if length == 0. {
0.
} else {
(((p.x - a.x) * d.x + (p.y - a.y) * d.y) / length).clamp(0., 1.)
};
p.distance(*a + d * t) <= self.radius
})
{
return Hit::Occluded;
}
if arrange {
return Hit::Destination(plan::Destination::LayoutOnly, None);
}
let hits: Vec<_> = self
.targets
.iter()
.enumerate()
.filter(|(_, t)| contains(t.region, p))
.map(|(i, _)| i)
.collect();
for (i, a) in hits.iter().enumerate() {
for b in &hits[i + 1..] {
let a = &self.targets[*a];
let b = &self.targets[*b];
if !a.ancestors.contains(&b.element) && !b.ancestors.contains(&a.element) {
return Hit::Ambiguous(hits);
}
}
}
match hits
.into_iter()
.max_by_key(|i| self.targets[*i].ancestors.len())
{
Some(i) => Hit::Destination(
plan::Destination::Performer(self.targets[i].element),
Some(i),
),
None => Hit::Destination(plan::Destination::OutsidePerformers, None),
}
}
}
#[derive(Debug, Clone)]
pub struct Press {
pub stamp: Stamp,
pub token: Id,
pub start: Point,
pub scope: plan::Scope,
pub frozen: Frozen,
pub arrange: bool,
}
#[derive(Debug, Clone)]
pub enum Event {
Press(Arc<Press>),
Preview(Stamp, Id, Option<Point>),
Release(Stamp, Id, Option<Point>),
Refused(u64, String),
Cancel(Id),
ArrangeNext,
Unfocused,
KeyboardBoundary,
}
pub struct Preview {
pub token: Id,
pub semantic: Arc<plan::SemanticPlan>,
pub placement: plan::PlacementPlan,
pub routed: Arc<RoutedView>,
}
struct Active {
press: Arc<Press>,
ids: BTreeMap<Id, Id>,
cache: Vec<(
plan::Destination,
Result<Arc<plan::SemanticPlan>, plan::Refusal>,
)>,
}
pub struct Feedback {
pub press: Arc<Press>,
pub targets: Vec<usize>,
pub text: String,
pub refused: bool,
}
#[derive(Default)]
pub struct State {
pub epoch: u64,
pub arrange_next: bool,
active: Option<Active>,
pub preview: Option<Arc<Preview>>,
pub feedback: Option<Feedback>,
}
#[derive(Clone, Copy)]
pub struct Capability<'a> {
pub state: &'a State,
pub clean: bool,
pub snapshot: usize,
}
impl State {
fn retire(&mut self) {
self.epoch = self.epoch.wrapping_add(1);
self.active = None;
self.preview = None;
self.feedback = None;
self.arrange_next = false;
}
}
pub fn refusal(r: &plan::Refusal) -> String {
match r {
plan::Refusal::Ineligible(s) | plan::Refusal::InvalidCandidate(s) | plan::Refusal::LayoutRequired(s) => s.clone(),
plan::Refusal::StaleScope => "The view changed. Start this drag again.".into(),
plan::Refusal::MixedLayoutRequired => "This work has a distributed or mixed assignment. Drag a specific leaf; arranging this group needs a layout step.".into(),
plan::Refusal::RoleExitRequired { message, .. } => message.clone(),
plan::Refusal::MissingAllocationId(_) => "Assignment identity expired. Start this drag again.".into(),
plan::Refusal::CollateralChangeRequired(records) => format!(
"Assignment needs dependent edits to {}. Resolve these before dragging again.",
records.iter().map(|r| r.name.as_str()).collect::<Vec<_>>().join(", ")
),
}
}
impl App {
pub fn allocation_drag_clean(&self) -> bool {
self.allocation_input_owner()
&& !self.draft_dirty()
&& !self.source_dirty()
&& !self.behavior.has_pending()
}
pub(crate) fn allocation_input_owner(&self) -> bool {
self.page == Page::Diagram
&& self
.active_diagram
.and_then(|id| self.project().diagrams.get(&id))
.is_some_and(|d| d.projection == ProjectionKind::Allocation)
&& self.architecture.is_none()
&& self.connection.is_none()
&& self.model_menu.is_none()
&& !self.reference_input_active()
&& !self.busy
&& !self.hosted.visible
&& self.workflow.is_none()
&& self.pending.is_none()
&& self.file_prompt.is_none()
&& !self.delete_pending
}
pub fn allocation_capability(&self) -> Capability<'_> {
Capability {
state: &self.allocation_drag,
clean: self.allocation_drag_clean(),
snapshot: Arc::as_ptr(&self.document.snapshot()) as usize,
}
}
fn allocation_stamp_valid(&self, stamp: Stamp) -> bool {
self.allocation_drag_clean()
&& self.route_context() == Some(stamp.context)
&& self.allocation_drag.epoch == stamp.epoch
&& Arc::as_ptr(&self.document.snapshot()) as usize == stamp.snapshot
&& self.pan == stamp.pan
&& self.zoom == stamp.zoom
&& self.canvas_size.is_none_or(|s| s == stamp.bounds.size())
&& stamp.bounds.width > 0.
&& stamp.bounds.height > 0.
}
/// Called before general input dispatch. Even equal-valued actions (Fit,
/// Undo/Redo, tool/modal round trips) retire old render-time capabilities.
pub(super) fn allocation_before(&mut self, message: &Message) {
if !matches!(
message,
Message::WorkDrag(_)
| Message::CheckpointTick
| Message::CheckpointWritten(..)
| Message::Lint(_)
| Message::Linted(..)
| Message::CanvasResized(_)
| Message::WindowResized(_)
| Message::SourceViewport
) {
self.allocation_drag.retire();
}
}
pub(super) fn allocation_boundary(&self) -> impl PartialEq + use<> {
(
self.route_context(),
self.pan,
self.zoom,
self.canvas_size,
self.window_size,
self.allocation_drag_clean(),
Arc::as_ptr(&self.document.snapshot()) as usize,
)
}
pub(super) fn allocation_retire(&mut self) {
self.allocation_drag.retire();
}
fn select_work_clean(&mut self, press: &Press) -> bool {
if !self.allocation_stamp_valid(press.stamp) {
return false;
}
self.selected = press.scope.work;
self.selected_node = Some(press.scope.placement);
self.selected_bend = None;
self.refresh_draft();
true
}
pub(super) fn allocation_event(&mut self, event: Event) -> Task<Message> {
match event {
Event::ArrangeNext => {
self.allocation_drag.retire();
if self.allocation_drag_clean() {
self.allocation_drag.arrange_next = true;
self.status = "Arrange next drag armed · moves work only · Esc cancels".into();
} else {
self.status = FINISH_EDIT.into();
}
}
Event::Unfocused => self.allocation_drag.retire(),
Event::KeyboardBoundary => {
if self.allocation_drag.active.is_some() {
self.allocation_drag.retire();
}
}
Event::Cancel(token) => {
if self
.allocation_drag
.active
.as_ref()
.is_some_and(|a| a.press.token == token)
{
self.allocation_drag.retire();
self.status = "Work drag canceled".into();
}
}
Event::Refused(epoch, text) => {
if epoch == self.allocation_drag.epoch {
self.allocation_drag.retire();
self.status = text;
}
}
Event::Press(press) => {
if self.allocation_drag.active.is_some() {
return Task::none();
}
if !self.allocation_stamp_valid(press.stamp)
|| press.arrange != self.allocation_drag.arrange_next
{
self.status = if self.allocation_drag_clean() {
"The view changed. Start this drag again.".into()
} else {
FINISH_EDIT.into()
};
return Task::none();
}
let checked = plan::capture(
self.project(),
press.scope.diagram.id,
press.scope.work,
press.scope.placement,
);
if checked.as_ref() != Ok(&press.scope) || !self.select_work_clean(&press) {
self.status = checked
.err()
.as_ref()
.map(refusal)
.unwrap_or_else(|| "The view changed. Start this drag again.".into());
self.allocation_drag.retire();
return Task::none();
}
let ids = press
.scope
.leaves
.keys()
.map(|id| (*id, Id::new_v4()))
.collect();
self.allocation_drag.arrange_next = false;
self.allocation_drag.preview = None;
self.allocation_drag.feedback = None;
self.allocation_drag.active = Some(Active {
press,
ids,
cache: Vec::new(),
});
}
Event::Preview(stamp, token, pointer) => {
self.allocation_motion(stamp, token, pointer, false)
}
Event::Release(stamp, token, pointer) => {
self.allocation_motion(stamp, token, pointer, true)
}
}
Task::none()
}
fn allocation_motion(
&mut self,
stamp: Stamp,
token: Id,
pointer: Option<Point>,
release: bool,
) {
if self
.allocation_drag
.active
.as_ref()
.is_none_or(|a| a.press.token != token)
{
return; // An old packet cannot cancel a newer gesture.
}
if !self.allocation_stamp_valid(stamp) {
self.allocation_drag.retire();
self.status = if self.allocation_drag_clean() {
"The view changed. Start this drag again.".into()
} else {
FINISH_EDIT.into()
};
return;
}
let Some(mut active) = self.allocation_drag.active.take() else {
return;
};
if active.press.token != token || active.press.stamp != stamp {
self.allocation_drag.active = Some(active);
return;
}
let press = active.press.clone();
let moved = pointer.is_some_and(|p| p.distance(press.start) > 3.);
let hit = press.frozen.resolve(stamp, pointer, press.arrange);
let mut targets = Vec::new();
let mut preview = None;
let result: Result<String, String> = match hit {
Hit::OutsideViewport => Err("Work drag canceled outside the canvas.".into()),
_ if !moved => Ok("Work selected".into()),
Hit::Occluded => Err("Aim at exposed performer space or empty canvas; this card, port, connection or control blocks the drop.".into()),
Hit::Ambiguous(indices) => {
targets = indices;
Err("Two unrelated performers overlap here. Use an exposed region.".into())
}
Hit::Destination(destination, target) => {
targets.extend(target);
let semantic = if let Some((_, result)) = active.cache.iter().find(|(d, _)| *d == destination) {
result.clone()
} else {
let result = plan::plan_semantics(self.project(), &press.scope, destination, &active.ids).map(Arc::new);
active.cache.push((destination, result.clone()));
result
};
semantic.map_err(|r| refusal(&r)).and_then(|semantic| {
if stamp.zoom * 16. < 9. {
return Err("Make room or adjust the view, then drag again. Zoom in to read the work and destination.".into());
}
let pointer = pointer.unwrap();
let delta = pointer - press.start;
let original = press.scope.placements[&press.scope.placement].position;
let requested = Position { x: original.x + delta.x / stamp.zoom, y: original.y + delta.y / stamp.zoom };
let presentation = plan::PresentationContext {
viewport: stamp.viewport(), zoom: stamp.zoom,
target: target.map(|i| {
let t = &press.frozen.targets[i];
plan::TargetPresentation { placement: t.placement, label: t.label }
}),
};
let placement = plan::place(&semantic, requested, presentation).map_err(|r| refusal(&r))?;
let label = if semantic.warnings.is_empty() { semantic.label.clone() } else {
format!("{} · {} model warning(s) retained", semantic.label, semantic.warnings.len())
};
if release {
let command = plan::finish(&semantic, self.project(), &placement).map_err(|r| refusal(&r))?;
if let Some(command) = command {
// Bypass applying selectors and unrelated repair helpers. The
// planner's exact Batch enters the authoritative source pipeline.
self.document.execute(command).map_err(|e| format!("{} · retry this drag after correcting the source issue", e))?;
self.sync();
self.selected = press.scope.work;
self.selected_node = Some(press.scope.placement);
self.refresh_draft();
}
} else {
let routed = Arc::new(routing::route_diagram(semantic.project(), &placement.diagram, &placement.view));
preview = Some(Arc::new(Preview { token, semantic, placement, routed }));
}
Ok(label)
})
}
};
let refused = result.is_err();
let text = result.unwrap_or_else(|text| text);
self.status = text.clone();
if release {
self.allocation_drag.retire();
} else {
self.allocation_drag.active = Some(active);
self.allocation_drag.preview = preview;
self.allocation_drag.feedback = Some(Feedback {
press,
targets,
text,
refused,
});
}
}
}
#[cfg(test)]
#[path = "allocation_drag_tests.rs"]
mod tests;
@@ -0,0 +1,290 @@
//! Actual App root, retained Iced tree, property widgets and production renderer.
//! Unlike packet-only tests, these prove the next key still reaches the field.
use super::*;
use crate::behavior::canvas_workspace::tests::{Mounted, key};
use iced::{Event as Input, keyboard};
fn character(text: &str, modifiers: keyboard::Modifiers) -> Input {
Input::Keyboard(keyboard::Event::KeyPressed {
key: keyboard::Key::Character(text.into()),
modified_key: keyboard::Key::Character(text.into()),
physical_key: keyboard::key::Physical::Code(keyboard::key::Code::KeyA),
location: keyboard::Location::Standard,
modifiers,
text: Some(text.into()),
repeat: false,
})
}
async fn mount() -> (Fixture, Mounted) {
let mut f = fixture(Layer::Oa);
f.app.window_size = iced::Size::new(2000., 1400.);
let _ = f.app.update(Message::Select(f.work));
let mut mounted = Mounted::new(&mut f.app).await;
// First sensor measurement intentionally Fits; now use a fixed known camera.
f.app.zoom = 1.;
f.app.pan = Vector::ZERO;
f.app.fit_to_view = false;
mounted.settle(&mut f.app).await;
(f, mounted)
}
fn canvas_bounds(mounted: &mut Mounted, app: &App) -> Rectangle {
mounted
.geometry(app)
.bounds(crate::allocation_input::CANVAS_ID)
}
fn at(mounted: &mut Mounted, f: &Fixture, world: Position) -> Point {
let bounds = canvas_bounds(mounted, &f.app);
Point::new(
bounds.x + world.x * f.app.zoom + f.app.pan.x,
bounds.y + world.y * f.app.zoom + f.app.pan.y,
)
}
fn name_field(mounted: &mut Mounted, app: &App) -> Rectangle {
mounted
.geometry(app)
.text_inputs
.into_iter()
.filter(|b| b.x > app.window_size.width - 320.)
.min_by(|a, b| a.y.total_cmp(&b.y))
.expect("actual inspector Name TextInput")
}
fn description_field(mounted: &mut Mounted, app: &App) -> Rectangle {
mounted
.geometry(app)
.focused
.into_iter()
.map(|(_, bounds, _)| bounds)
.filter(|b| b.x > app.window_size.width - 320. && b.height >= 90.)
.min_by(|a, b| a.y.total_cmp(&b.y))
.expect("actual inspector Description TextEditor")
}
fn is_focused(mounted: &mut Mounted, app: &App, field: Rectangle) -> bool {
mounted
.geometry(app)
.focused
.iter()
.any(|(_, bounds, focused)| *bounds == field && *focused)
}
async fn type_key(mounted: &mut Mounted, app: &mut App, text: &str) -> Vec<Message> {
mounted
.batch(
app,
&[character(text, keyboard::Modifiers::empty())],
mouse::Cursor::Unavailable,
)
.await
}
async fn refused_click_keeps_field(mounted: &mut Mounted, f: &mut Fixture, field: Rectangle) {
let before = f.app.document.bundle().clone();
let selected = (f.app.selected, f.app.selected_node);
let point = at(mounted, f, xy(140., 535.));
let messages = mounted.click(&mut f.app, point).await;
assert!(
messages
.iter()
.any(|m| matches!(m, Message::WorkDrag(Event::Refused(..))))
);
assert!(!messages.iter().any(|m| matches!(
m,
Message::Select(_) | Message::SelectNode(_) | Message::MoveNode(..)
)));
assert_eq!(f.app.document.bundle(), &before);
assert_eq!((f.app.selected, f.app.selected_node), selected);
assert!(
is_focused(mounted, &f.app, field),
"refused press must not reach the outside-press unfocus handler"
);
}
#[tokio::test]
async fn actual_app_tree_dirty_name_retains_focus_and_middle_caret_for_next_key() {
let (mut f, mut mounted) = mount().await;
let field = name_field(&mut mounted, &f.app);
mounted.click(&mut f.app, field.center()).await;
mounted
.batch(
&mut f.app,
&[key(keyboard::key::Named::Home)],
mouse::Cursor::Unavailable,
)
.await;
type_key(&mut mounted, &mut f.app, "X").await;
mounted
.batch(
&mut f.app,
&[key(keyboard::key::Named::ArrowLeft)],
mouse::Cursor::Unavailable,
)
.await;
assert_eq!(f.app.draft.as_ref().unwrap().inputs["/name"].raw, "XWork");
refused_click_keeps_field(&mut mounted, &mut f, field).await;
let messages = type_key(&mut mounted, &mut f.app, "Y").await;
assert!(
messages
.iter()
.any(|m| matches!(m, Message::Field(path, _) if path == "/name"))
);
assert_eq!(f.app.draft.as_ref().unwrap().inputs["/name"].raw, "YXWork");
assert!(!f.app.document.can_undo());
}
#[tokio::test]
async fn actual_app_tree_dirty_description_retains_focus_and_middle_caret_for_next_key() {
let (mut f, mut mounted) = mount().await;
let field = description_field(&mut mounted, &f.app);
mounted
.click(&mut f.app, Point::new(field.x + 16., field.y + 16.))
.await;
type_key(&mut mounted, &mut f.app, "A").await;
type_key(&mut mounted, &mut f.app, "B").await;
mounted
.batch(
&mut f.app,
&[key(keyboard::key::Named::ArrowLeft)],
mouse::Cursor::Unavailable,
)
.await;
assert_eq!(
f.app.draft.as_ref().unwrap().inputs["/description"].raw,
"AB"
);
refused_click_keeps_field(&mut mounted, &mut f, field).await;
let messages = type_key(&mut mounted, &mut f.app, "X").await;
assert!(
messages
.iter()
.any(|m| matches!(m, Message::FieldText(path, _) if path == "/description"))
);
assert_eq!(
f.app.draft.as_ref().unwrap().inputs["/description"].raw,
"AXB"
);
assert!(!f.app.document.can_undo());
}
#[tokio::test]
async fn actual_keyboard_arrange_shortcut_consumes_once_escape_cancels_and_dirty_owner_refuses() {
let (mut f, mut mounted) = mount().await;
let shortcut = character("a", keyboard::Modifiers::ALT);
let messages = mounted
.batch(&mut f.app, &[shortcut.clone()], mouse::Cursor::Unavailable)
.await;
assert!(
messages
.iter()
.any(|m| matches!(m, Message::WorkDrag(Event::ArrangeNext)))
);
assert!(f.app.allocation_drag.arrange_next);
mounted
.batch(
&mut f.app,
&[key(keyboard::key::Named::Escape)],
mouse::Cursor::Unavailable,
)
.await;
assert!(!f.app.allocation_drag.arrange_next);
mounted
.batch(&mut f.app, &[shortcut.clone()], mouse::Cursor::Unavailable)
.await;
let from = at(&mut mounted, &f, xy(140., 535.));
let to = from + Vector::new(30., 0.);
let messages = mounted
.batch(
&mut f.app,
&[
Input::Mouse(mouse::Event::CursorMoved { position: from }),
Input::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
Input::Mouse(mouse::Event::CursorMoved { position: to }),
Input::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)),
],
mouse::Cursor::Available(to),
)
.await;
assert!(
messages
.iter()
.any(|m| matches!(m, Message::WorkDrag(Event::Press(p)) if p.arrange))
);
assert!(!f.app.allocation_drag.arrange_next);
assert_eq!(f.app.document.revision(), 1, "{}", f.app.status);
assert!(f.app.project().relations.is_empty());
let field = name_field(&mut mounted, &f.app);
mounted.click(&mut f.app, field.center()).await;
type_key(&mut mounted, &mut f.app, "X").await;
let raw = f.app.draft.as_ref().unwrap().inputs["/name"].raw.clone();
let before = f.app.document.bundle().clone();
mounted
.batch(&mut f.app, &[shortcut], mouse::Cursor::Unavailable)
.await;
assert!(!f.app.allocation_drag.arrange_next);
assert_eq!(f.app.status, FINISH_EDIT);
assert!(is_focused(&mut mounted, &f.app, field));
assert_eq!(f.app.draft.as_ref().unwrap().inputs["/name"].raw, raw);
assert_eq!(f.app.document.bundle(), &before);
}
fn canvas_pixels(pixels: &[u8], width: usize, canvas: Rectangle) -> Vec<u8> {
let x = canvas.x.ceil() as usize;
let y = canvas.y.ceil() as usize;
let right = (canvas.x + canvas.width).floor() as usize;
let bottom = (canvas.y + canvas.height).floor() as usize;
(y..bottom)
.flat_map(|row| {
pixels[(row * width + x) * 4..(row * width + right) * 4]
.iter()
.copied()
})
.collect()
}
#[tokio::test]
async fn actual_rendered_canvas_pixels_do_not_change_when_toolbar_feedback_text_changes() {
let (mut f, mut mounted) = mount().await;
let mut project = f.app.project().clone();
let target = project
.diagrams
.get_mut(&f.diagram)
.unwrap()
.nodes
.values_mut()
.find(|n| n.element == f.a)
.unwrap();
target.position.y = -41.;
target.size.height = 300.;
replace(&mut f, project);
f.app.zoom = 0.75;
f.app.pan = Vector::new(15., 15.);
let from = at(&mut mounted, &f, xy(140., 535.));
let to = at(&mut mounted, &f, xy(200., 80.));
mounted
.batch(
&mut f.app,
&[
Input::Mouse(mouse::Event::CursorMoved { position: from }),
Input::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
Input::Mouse(mouse::Event::CursorMoved { position: to }),
],
mouse::Cursor::Available(to),
)
.await;
let feedback = f
.app
.allocation_drag
.feedback
.as_ref()
.expect("active frozen destination");
assert!(!feedback.refused, "{}", feedback.text);
let target = &feedback.press.frozen.targets[feedback.targets[0]];
assert!(target.label.position.y * f.app.zoom + f.app.pan.y < 14.);
let canvas = canvas_bounds(&mut mounted, &f.app);
let first = mounted.pixels(&f.app);
f.app.allocation_drag.feedback.as_mut().unwrap().text =
"Very different drag status, retained outside the readable canvas. ".repeat(10);
let second = mounted.pixels(&f.app);
assert_ne!(first, second, "the actual toolbar status changes");
assert_eq!(
canvas_pixels(&first, f.app.window_size.width as usize, canvas),
canvas_pixels(&second, f.app.window_size.width as usize, canvas),
"status must not obscure any candidate header or frozen target label"
);
}
File diff suppressed because it is too large Load Diff
@@ -77,6 +77,7 @@ fn architecture_f2_routes_selected_node_to_inline_rename_and_exact_undo() {
editable: true,
placing: false,
connection: None,
allocation: None,
};
let mut state = crate::diagram::State::default();
let key = CanvasEvent::Keyboard(keyboard::Event::KeyPressed {
@@ -232,6 +232,7 @@ fn architecture_canvas_left_click_uses_world_coordinate_once_at_zoom_and_pan() {
editable: true,
placing: true,
connection: None,
allocation: None,
};
let mut state = crate::diagram::State::default();
let action = canvas
@@ -306,6 +307,7 @@ fn architecture_f2_rename_preserves_identity_placement_and_atomic_history() {
editable: true,
placing: false,
connection: None,
allocation: None,
};
let bounds = iced::Rectangle::with_size(iced::Size::new(800., 600.));
let cursor = mouse::Cursor::Available(iced::Point::new(500., 400.));
@@ -291,7 +291,7 @@ fn pending_behavior_blocks_navigation_property_edits_and_model_history() {
Message::BrowseGroup(crate::explorer::Group::Work),
Message::Field("/name".into(), "Overwrite properties".into()),
Message::Command(Command::Rename(fixture.source, "Overwrite model".into())),
Message::ConfirmDelete,
Message::ConfirmDelete(0),
Message::Undo,
Message::Redo,
Message::ShortcutUndo(false),
+5
View File
@@ -214,6 +214,11 @@ impl App {
.is_some_and(architecture_authoring::State::inline)
{
Task::batch([lint, iced::widget::operation::focus("architecture-name")])
} else if self.page == Page::Behavior && self.behavior.source_open() {
Task::batch([
lint,
iced::widget::operation::focus(behavior::source_draft::NAME_INPUT),
])
} else if self.page == Page::Behavior && self.behavior.creation.created().is_some() {
Task::batch([
lint,
@@ -0,0 +1,152 @@
//! Captured diagram removal and model-confirmation lifetimes.
use super::*;
use crate::{model_menu, route_edit::Context};
#[derive(Debug, Clone)]
pub struct Intent {
pub context: Context,
pub object: Id,
pub placement: Option<Id>,
pub model: bool,
}
pub struct Confirmation {
pub snapshot: model_menu::Opened,
pub selection: Id,
pub placement: Option<Id>,
}
pub fn enabled(app: &App) -> bool {
crate::behavior::canvas_workspace::keyboard_enabled(app)
&& !app.draft_dirty()
&& !app.source_dirty()
&& !app.behavior.has_pending()
&& app
.architecture
.as_ref()
.is_none_or(|state| !state.inline())
}
impl App {
pub(super) fn remove_from_diagram(&mut self, intent: Intent) {
if !enabled(self) || self.connection.is_some() || self.selected_bend.is_some() {
return;
}
let current = match self.page {
Page::Diagram if self.architecture.as_ref().is_none_or(|s| !s.saved.placing) => {
self.route_context()
}
Page::Behavior
if self.behavior.diagram.tool == crate::behavior::state_edit::Tool::Select =>
{
self.state_context()
}
_ => None,
};
if current != Some(intent.context) || self.selected != intent.object {
return;
}
let view = projection::project_view(self.project(), intent.context.diagram);
let placement = if self.page == Page::Behavior {
self.behavior.diagram.placement
} else {
self.selected_node
};
if intent.placement != placement || intent.object == intent.context.diagram {
return;
}
let visible = if let Some(id) = intent.placement {
self.project().diagrams[&intent.context.diagram]
.nodes
.get(&id)
.is_some_and(|node| node.element == intent.object)
&& view
.nodes
.iter()
.any(|node| node.id == id && node.element == intent.object)
} else {
view.edges.iter().any(|edge| edge.id == intent.object)
|| view.ports.iter().any(|port| port.element == intent.object)
};
if !visible {
return;
}
if !intent.model
&& let Some(placement) = intent.placement
{
if self.execute(Command::Delete(placement)) {
self.selected_node = None;
self.behavior.diagram.placement = None;
self.selected_bend = None;
self.status = "Hidden from this diagram · Model element preserved · Undo restores the placement".into();
}
} else {
self.begin_model_delete(Some(intent.context));
}
}
pub(super) fn begin_model_delete(&mut self, canvas: Option<Context>) {
if self.selected == self.project().id || self.behavior.has_pending() {
return;
}
let mut request = model_menu::Request::object(self.selected, iced::Point::ORIGIN);
request.canvas = canvas;
request.placement = if self.page == Page::Behavior {
self.behavior.diagram.placement
} else {
self.selected_node
};
// Opening a modal retires any pending canvas intent before capturing
// the confirmation; every new dialog receives its own replay token.
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
if canvas.is_some() {
request.canvas = if self.page == Page::Behavior {
self.state_context()
} else {
self.route_context()
};
}
self.menu_sequence = self.menu_sequence.wrapping_add(1);
self.delete_target =
model_menu::Opened::new(self, request, self.menu_sequence).map(|snapshot| {
Confirmation {
snapshot,
selection: self.selected,
placement: if self.page == Page::Behavior {
self.behavior.diagram.placement
} else {
self.selected_node
},
}
});
self.delete_pending = self.delete_target.is_some();
}
pub(super) fn confirm_model_delete(&mut self, token: u64) {
let Some(target) = self.delete_target.as_ref() else {
return;
};
if target.snapshot.token != token {
return;
}
let valid = self.delete_pending
&& target.snapshot.valid(self)
&& target.selection == self.selected
&& target.placement
== if self.page == Page::Behavior {
self.behavior.diagram.placement
} else {
self.selected_node
}
&& !self.behavior.has_pending()
&& !self.source_dirty()
&& !self.draft_dirty();
let id = target.snapshot.request.object;
self.delete_pending = false;
self.delete_target = None;
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
if valid {
self.execute(Command::Delete(id));
}
}
}
@@ -0,0 +1,681 @@
//! Real App tree regression tests; no private canvas focus or gesture seeding.
use super::*;
use crate::behavior::canvas_workspace::tests::{Mounted, key};
use iced::advanced::widget::{Operation, operation};
use iced::{Event as Input, Point, Rectangle, Size, Vector, keyboard, mouse};
struct Fixture {
app: App,
object: Id,
node: Id,
other: Id,
diagram: Id,
copy: Id,
}
fn fixture(layer: Layer) -> Fixture {
let mut p = Project::empty();
let kind = if layer == Layer::Oa {
Kind::Entity
} else {
Kind::System
};
let object = p.add_element(kind, layer, "Target", None).unwrap();
let other = p
.add_element(
if layer == Layer::Oa {
Kind::Actor
} else {
Kind::SystemActor
},
layer,
"Other",
None,
)
.unwrap();
let diagram = p.ensure_diagram(layer);
p.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::Overview;
let node = p.place(diagram, object, Position { x: 80., y: 80. });
p.place(diagram, other, Position { x: 500., y: 80. });
let mut second = p.diagrams[&diagram].clone();
second.id = Id::new_v4();
second.name = "Other diagram".into();
second.alias = "other_diagram".into();
second.nodes.clear();
second.routes.clear();
let copy = second.id;
p.diagrams.insert(copy, second);
p.place(copy, object, Position { x: 70., y: 70. });
let mut app = App::with_project(p);
app.page = Page::Diagram;
app.layer = layer;
app.active_diagram = Some(diagram);
app.window_size = Size::new(2000., 1400.);
Fixture {
app,
object,
node,
other,
diagram,
copy,
}
}
async fn mount(layer: Layer) -> (Fixture, Mounted) {
let mut f = fixture(layer);
let mut mounted = Mounted::new(&mut f.app).await;
f.app.zoom = 1.;
f.app.pan = Vector::ZERO;
f.app.fit_to_view = false;
mounted.settle(&mut f.app).await;
select(&mut mounted, &mut f).await;
(f, mounted)
}
async fn select(mounted: &mut Mounted, f: &mut Fixture) {
let bounds = mounted
.geometry(&f.app)
.bounds(crate::allocation_input::CANVAS_ID);
let node = projection::project_view(f.app.project(), f.diagram)
.nodes
.into_iter()
.find(|node| node.id == f.node)
.unwrap();
let point = Point::new(
bounds.x + (node.position.x + 40.) * f.app.zoom + f.app.pan.x,
bounds.y + (node.position.y + 35.) * f.app.zoom + f.app.pan.y,
);
mounted.click(&mut f.app, point).await;
assert_eq!(f.app.selected, f.object);
assert_eq!(f.app.selected_node, Some(f.node));
}
fn modified(named: keyboard::key::Named, modifiers: keyboard::Modifiers) -> Input {
let mut input = key(named);
if let Input::Keyboard(keyboard::Event::KeyPressed {
modifiers: value, ..
}) = &mut input
{
*value = modifiers;
}
input
}
async fn delete(mounted: &mut Mounted, app: &mut App, shift: bool) -> Vec<Message> {
mounted
.batch(
app,
&[modified(
keyboard::key::Named::Delete,
if shift {
keyboard::Modifiers::SHIFT
} else {
keyboard::Modifiers::empty()
},
)],
mouse::Cursor::Unavailable,
)
.await
}
fn consistent(app: &App) {
assert_eq!(
source_project::compile_bundle(app.document.bundle(), LIMITS)
.unwrap()
.decoded
.project,
*app.project()
);
assert_eq!(
source_workspace::reachable(&app.source_workspace.bundle()).unwrap(),
*app.document.bundle()
);
}
#[tokio::test]
async fn mounted_oa_sa_delete_hides_exact_placement_and_round_trips_source_and_model() {
for layer in [Layer::Oa, Layer::Sa] {
let (mut f, mut mounted) = mount(layer).await;
let before = f.app.document.snapshot();
let messages = delete(&mut mounted, &mut f.app, false).await;
assert!(
messages
.iter()
.any(|message| matches!(message, Message::DiagramRemove(_)))
);
assert!(f.app.project().contains(f.object));
assert!(
!f.app.project().diagrams[&f.diagram]
.nodes
.contains_key(&f.node)
);
assert!(
f.app.project().diagrams[&f.copy]
.nodes
.values()
.any(|node| node.element == f.object)
);
assert!(!f.app.delete_pending);
consistent(&f.app);
let after = f.app.document.snapshot();
let task = f.app.update(Message::Undo);
mounted.perform(&mut f.app, task).await;
assert_eq!(f.app.project(), &before.decoded.project);
assert_eq!(f.app.document.bundle(), &before.bundle);
assert!(!f.app.document.can_undo(), "one removal, one undo unit");
let task = f.app.update(Message::Redo);
mounted.perform(&mut f.app, task).await;
assert_eq!(f.app.project(), &after.decoded.project);
assert_eq!(f.app.document.bundle(), &after.bundle);
consistent(&f.app);
}
}
#[tokio::test]
async fn mounted_shift_delete_captures_target_cancel_escape_and_confirmation_replay() {
let (mut f, mut mounted) = mount(Layer::Oa).await;
let before = f.app.document.snapshot();
delete(&mut mounted, &mut f.app, true).await;
let target = f.app.delete_target.as_ref().unwrap();
assert_eq!(target.snapshot.request.object, f.object);
let stale_token = target.snapshot.token;
let task = f.app.update(Message::CancelDelete);
mounted.perform(&mut f.app, task).await;
let task = f.app.update(Message::ConfirmDelete(stale_token));
mounted.perform(&mut f.app, task).await;
assert_eq!(f.app.document.bundle(), &before.bundle);
select(&mut mounted, &mut f).await;
delete(&mut mounted, &mut f.app, true).await;
let token = f.app.delete_target.as_ref().unwrap().snapshot.token;
let task = f.app.update(Message::ConfirmDelete(stale_token));
mounted.perform(&mut f.app, task).await;
assert!(
f.app.delete_pending,
"old dialog must not accept a new confirmation"
);
let task = f.app.update(Message::ConfirmDelete(token));
mounted.perform(&mut f.app, task).await;
assert!(!f.app.project().contains(f.object));
assert!(f.app.project().contains(f.other));
consistent(&f.app);
let task = f.app.update(Message::Undo);
mounted.perform(&mut f.app, task).await;
assert_eq!(f.app.document.bundle(), &before.bundle);
select(&mut mounted, &mut f).await;
delete(&mut mounted, &mut f.app, true).await;
let task = f.app.update(Message::Escape);
mounted.perform(&mut f.app, task).await;
assert!(!f.app.delete_pending);
assert!(f.app.delete_target.is_none());
assert_eq!(f.app.document.bundle(), &before.bundle);
}
struct FocusField(Rectangle);
impl Operation for FocusField {
fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn Operation)) {
visit(self);
}
fn focusable(
&mut self,
_: Option<&iced::advanced::widget::Id>,
bounds: Rectangle,
state: &mut dyn operation::Focusable,
) {
if bounds == self.0 {
state.focus();
} else {
state.unfocus();
}
}
}
fn name_field(mounted: &mut Mounted, app: &App) -> Rectangle {
mounted
.geometry(app)
.text_inputs
.into_iter()
.filter(|bounds| bounds.x > app.window_size.width - 320.)
.min_by(|a, b| a.y.total_cmp(&b.y))
.unwrap()
}
#[tokio::test]
async fn mounted_clean_text_programmatic_focus_and_tab_remove_canvas_keyboard_ownership() {
for programmatic in [true, false] {
let (mut f, mut mounted) = mount(Layer::Oa).await;
let before = f.app.document.snapshot();
let field = name_field(&mut mounted, &f.app);
if programmatic {
mounted.operate(&f.app, &mut FocusField(field));
} else {
let messages = mounted
.batch(
&mut f.app,
&[
key(keyboard::key::Named::Tab),
key(keyboard::key::Named::Delete),
],
mouse::Cursor::Unavailable,
)
.await;
assert!(
!messages
.iter()
.any(|message| matches!(message, Message::DiagramRemove(_))),
"Tab retires canvas admission before its queued focus operation runs"
);
}
let geometry = mounted.geometry(&f.app);
assert!(
geometry
.focused
.iter()
.any(|(_, bounds, focused)| *bounds == field && *focused),
"clean Inspector receives real focus"
);
assert!(!f.app.draft_dirty());
assert!(
delete(&mut mounted, &mut f.app, true)
.await
.iter()
.all(|message| !matches!(message, Message::DiagramRemove(_)))
);
assert!(!f.app.delete_pending);
mounted
.batch(
&mut f.app,
&[key(keyboard::key::Named::Home)],
mouse::Cursor::Unavailable,
)
.await;
let messages = delete(&mut mounted, &mut f.app, false).await;
assert!(
messages
.iter()
.any(|message| matches!(message, Message::Field(path, _) if path == "/name"))
);
assert_eq!(f.app.draft.as_ref().unwrap().inputs["/name"].raw, "arget");
assert_eq!(f.app.document.bundle(), &before.bundle);
assert!(f.app.project().contains(f.node));
assert!(!f.app.delete_pending);
}
}
#[tokio::test]
async fn mounted_removal_intents_reject_stale_selection_revision_owner_and_document() {
for boundary in 0..5 {
let (mut f, mut mounted) = mount(Layer::Oa).await;
let messages = mounted.input(
&f.app,
&[key(keyboard::key::Named::Delete)],
mouse::Cursor::Unavailable,
);
let intent = messages
.into_iter()
.find(|message| matches!(message, Message::DiagramRemove(_)))
.unwrap();
match boundary {
0 => {
let _ = f.app.update(Message::Select(f.other));
}
1 => {
f.app.canvas_epoch = f.app.canvas_epoch.wrapping_add(1);
}
2 => {
f.app.active_diagram = Some(f.copy);
}
3 => {
f.app.canvas_document = Id::new_v4();
}
_ => {
let _ = f
.app
.update(Message::Command(Command::Rename(f.other, "Renamed".into())));
}
}
let before = f.app.document.snapshot();
let task = f.app.update(intent);
mounted.perform(&mut f.app, task).await;
assert_eq!(f.app.document.bundle(), &before.bundle);
assert!(f.app.project().contains(f.node));
assert!(!f.app.delete_pending);
}
}
#[tokio::test]
async fn mounted_window_modal_dirty_and_modifier_boundaries_retire_delete_focus() {
for boundary in 0..5 {
let (mut f, mut mounted) = mount(Layer::Oa).await;
let before = f.app.document.snapshot();
match boundary {
0 => {
mounted
.batch(
&mut f.app,
&[Input::Window(iced::window::Event::Unfocused)],
mouse::Cursor::Unavailable,
)
.await;
}
1 => {
let _ = f.app.update(Message::Menu(crate::model_menu::Event::Open(
crate::model_menu::Request::object(f.object, Point::ORIGIN),
)));
mounted.settle(&mut f.app).await;
let _ = f.app.update(Message::Menu(crate::model_menu::Event::Close));
}
2 => {
let _ = f.app.update(Message::Field("/name".into(), "Dirty".into()));
}
3 => {
let _ = f.app.update(Message::Delete);
}
_ => {
f.app.canvas_epoch = f.app.canvas_epoch.wrapping_add(1);
}
}
delete(&mut mounted, &mut f.app, false).await;
assert_eq!(f.app.document.bundle(), &before.bundle);
assert!(f.app.project().contains(f.node));
}
let (mut f, mut mounted) = mount(Layer::Oa).await;
let before = f.app.document.snapshot();
mounted
.batch(
&mut f.app,
&[modified(
keyboard::key::Named::Delete,
keyboard::Modifiers::CTRL,
)],
mouse::Cursor::Unavailable,
)
.await;
assert_eq!(f.app.document.bundle(), &before.bundle);
assert!(!f.app.delete_pending);
// Refusal is temporary: a fresh live canvas click owns Delete again.
select(&mut mounted, &mut f).await;
delete(&mut mounted, &mut f.app, false).await;
assert!(!f.app.project().contains(f.node));
}
#[tokio::test]
async fn mounted_bend_delete_has_priority_and_semantic_edge_delete_is_confirmed() {
let mut p = Project::empty();
let a = p.add(Kind::Activity, "Send", 0., 0.);
let b = p.add(Kind::Activity, "Receive", 650., 0.);
let edge = p.relate(RelationKind::Interaction, "Report", a, b);
let diagram = p.ensure_diagram(Layer::Oa);
let p = execute_batch(
&p,
&[Command::SetEdgeRoute {
diagram,
route: EdgeRoute {
edge,
waypoints: vec![Position { x: 400., y: -100. }],
label: None,
},
}],
)
.unwrap();
let mut app = App::with_project(p);
app.page = Page::Diagram;
app.active_diagram = Some(diagram);
app.window_size = Size::new(2000., 1400.);
let _ = app.update(Message::Select(edge));
let mut mounted = Mounted::new(&mut app).await;
app.zoom = 1.;
app.pan = Vector::new(100., 250.);
app.fit_to_view = false;
mounted.settle(&mut app).await;
let bounds = mounted
.geometry(&app)
.bounds(crate::allocation_input::CANVAS_ID);
let pin = Point::new(bounds.x + 500., bounds.y + 150.);
mounted.click(&mut app, pin).await;
assert_eq!(
app.selected_bend,
Some((edge, 0)),
"real pointer selects route pin"
);
let before = app.document.snapshot();
let messages = delete(&mut mounted, &mut app, false).await;
assert!(messages.iter().any(|message| matches!(
message,
Message::Route(crate::route_edit::Event::RemoveBend)
)));
assert!(
!messages
.iter()
.any(|message| matches!(message, Message::DiagramRemove(_)))
);
assert!(app.project().contains(edge) && app.project().contains(a) && app.project().contains(b));
assert!(
crate::route_edit::stored_route(&app.project().diagrams[&diagram], edge)
.waypoints
.is_empty()
);
assert!(!app.delete_pending);
consistent(&app);
let task = app.update(Message::Undo);
mounted.perform(&mut app, task).await;
assert_eq!(app.document.bundle(), &before.bundle);
// Select a real edge segment away from its handle, then Delete opens the
// same semantic confirmation instead of inventing an edge hide operation.
let view = projection::project_view(app.project(), diagram);
let routed = routing::route_diagram(app.project(), &app.project().diagrams[&diagram], &view);
let route = &routed.edges[&edge];
let segment = route
.points
.windows(2)
.max_by(|a, b| {
let len = |s: &[Position]| (s[1].x - s[0].x).abs() + (s[1].y - s[0].y).abs();
len(a).total_cmp(&len(b))
})
.unwrap();
let at = Point::new(
bounds.x + (segment[0].x + segment[1].x) / 2. + app.pan.x,
bounds.y + (segment[0].y + segment[1].y) / 2. + app.pan.y,
);
mounted.click(&mut app, at).await;
assert_eq!(app.selected, edge);
assert!(app.selected_bend.is_none());
delete(&mut mounted, &mut app, false).await;
assert_eq!(
app.delete_target.as_ref().unwrap().snapshot.request.object,
edge
);
assert_eq!(app.document.bundle(), &before.bundle);
}
#[tokio::test]
async fn mounted_state_delete_uses_state_placement_and_preserves_architecture_placement() {
let mut p = Project::empty();
let machine = p
.add_element(Kind::StateMachine, Layer::Sa, "Machine", None)
.unwrap();
let region = p
.add_element(Kind::Region, Layer::Sa, "Main", Some(machine))
.unwrap();
let state = p
.add_element(Kind::State, Layer::Sa, "Ready", Some(region))
.unwrap();
let architecture = p.ensure_diagram(Layer::Sa);
let architecture_node = p.place(architecture, state, Position { x: 50., y: 50. });
let plan = crate::behavior::state_edit::create_view(&p, machine).unwrap();
let diagram = plan.diagram;
let p = execute_batch(&p, &[plan.command]).unwrap();
let node = p.diagrams[&diagram]
.nodes
.values()
.find(|node| node.element == state)
.unwrap()
.id;
let mut app = App::with_project(p);
app.layer = Layer::Sa;
app.page = Page::Behavior;
app.active_diagram = Some(architecture);
app.behavior.active_owner = Some(machine);
app.behavior.diagram.diagram = Some(diagram);
app.window_size = Size::new(2000., 1400.);
let _ = app.update(Message::Select(state));
let mut mounted = Mounted::new(&mut app).await;
let bounds = mounted
.geometry(&app)
.focused
.into_iter()
.find(|(id, _, _)| {
id.as_ref() == Some(&iced::advanced::widget::Id::from("diagram-keyboard-owner"))
})
.unwrap()
.1;
let projected = projection::project_view(app.project(), diagram)
.nodes
.into_iter()
.find(|n| n.id == node)
.unwrap();
let at = Point::new(
bounds.x
+ (projected.position.x + 35.) * app.behavior.diagram.zoom
+ app.behavior.diagram.pan.x,
bounds.y
+ (projected.position.y + 35.) * app.behavior.diagram.zoom
+ app.behavior.diagram.pan.y,
);
mounted.click(&mut app, at).await;
assert_eq!(app.selected, state);
assert_eq!(app.behavior.diagram.placement, Some(node));
let before = app.document.snapshot();
let messages = delete(&mut mounted, &mut app, false).await;
assert!(messages.iter().any(|message| matches!(
message,
Message::StateDiagram(crate::behavior::state_edit::Event::Remove(_))
)));
assert!(!app.project().contains(node));
assert!(app.project().contains(architecture_node) && app.project().contains(state));
consistent(&app);
let task = app.update(Message::Undo);
mounted.perform(&mut app, task).await;
assert_eq!(app.document.bundle(), &before.bundle);
}
#[tokio::test]
async fn captured_confirmation_rejects_selection_document_revision_and_placement_changes() {
for boundary in 0..5 {
let (mut f, mut mounted) = mount(Layer::Oa).await;
delete(&mut mounted, &mut f.app, true).await;
let token = f.app.delete_target.as_ref().unwrap().snapshot.token;
// Model asynchronous adoption / retained stale UI using a changed App
// state while preserving the old confirmation payload deliberately.
match boundary {
0 => {
f.app.selected = f.other;
}
1 => {
f.app.canvas_document = Id::new_v4();
}
2 => {
f.app.canvas_epoch = f.app.canvas_epoch.wrapping_add(1);
}
3 => {
f.app.selected_node = None;
}
_ => {
assert!(
f.app
.document
.execute(Command::Rename(f.other, "New revision".into()))
.is_ok()
);
}
}
let before = f.app.document.snapshot();
let task = f.app.update(Message::ConfirmDelete(token));
mounted.perform(&mut f.app, task).await;
assert_eq!(f.app.document.bundle(), &before.bundle);
assert!(f.app.project().contains(f.object));
assert!(!f.app.delete_pending);
}
}
#[tokio::test]
async fn mounted_programmatic_canvas_entry_needs_no_mouse_focus_boolean() {
let mut f = fixture(Layer::Oa);
let _ = f.app.update(Message::Select(f.object));
let mut mounted = Mounted::new(&mut f.app).await;
let task =
iced::widget::operation::focus(iced::advanced::widget::Id::from("diagram-keyboard-owner"));
mounted.perform(&mut f.app, task).await;
let messages = delete(&mut mounted, &mut f.app, false).await;
assert!(
messages
.iter()
.any(|message| matches!(message, Message::DiagramRemove(_)))
);
assert!(!f.app.project().contains(f.node));
assert!(f.app.project().contains(f.object));
}
#[tokio::test]
async fn mounted_allocation_work_click_then_delete_without_redraw_removes_placement_once() {
for layer in [Layer::Oa, Layer::Sa] {
let mut p = Project::empty();
let performer = p
.add_element(
if layer == Layer::Oa {
Kind::Entity
} else {
Kind::System
},
layer,
"Performer",
None,
)
.unwrap();
let work = p
.add_element(
if layer == Layer::Oa {
Kind::Activity
} else {
Kind::Function
},
layer,
"Work",
None,
)
.unwrap();
let diagram = p.ensure_diagram(layer);
p.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::Allocation;
p.place(diagram, performer, Position { x: 100., y: 100. });
let node = p.place(diagram, work, Position { x: 100., y: 500. });
let mut app = App::with_project(p);
app.page = Page::Diagram;
app.layer = layer;
app.active_diagram = Some(diagram);
app.window_size = Size::new(2000., 1400.);
let mut mounted = Mounted::new(&mut app).await;
app.zoom = 1.;
app.pan = Vector::ZERO;
app.fit_to_view = false;
mounted.settle(&mut app).await;
let bounds = mounted
.geometry(&app)
.bounds(crate::allocation_input::CANVAS_ID);
let point = Point::new(bounds.x + 140., bounds.y + 535.);
let messages = mounted.click(&mut app, point).await;
assert!(messages.iter().any(|message| matches!(
message,
Message::WorkDrag(crate::app::allocation_drag::Event::Press(_))
)));
assert_eq!(app.selected, work);
assert_eq!(app.selected_node, Some(node));
let before = app.document.snapshot();
// Deliberately do not settle/redraw between completed click and key.
let messages = delete(&mut mounted, &mut app, false).await;
assert!(
messages
.iter()
.any(|message| matches!(message, Message::DiagramRemove(_))),
"retired allocation press cannot consume first Delete"
);
assert!(!app.project().contains(node));
assert!(app.project().contains(work) && app.project().contains(performer));
consistent(&app);
let task = app.update(Message::Undo);
mounted.perform(&mut app, task).await;
assert_eq!(app.document.bundle(), &before.bundle);
assert!(!app.document.can_undo());
}
}
+79 -35
View File
@@ -15,18 +15,15 @@ impl App {
epoch: self.canvas_epoch,
})
}
pub(super) fn reveal_process_selection(&self) -> Task<Message> {
let Some(context) = self.topology_context() else {
return Task::none();
};
let Some(offset) =
self.behavior
.topology
.selection_offset(self.project(), context.owner, self.selected)
else {
return Task::none();
};
iced::widget::operation::scroll_to("process-canvas", offset)
pub(super) fn reveal_process_selection(&mut self) -> Task<Message> {
if let Some(context) = self.topology_context() {
let mut state = std::mem::take(&mut self.behavior.topology);
if state.reveal_selection(self.project(), context.owner, self.selected) {
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
}
self.behavior.topology = state;
}
Task::none()
}
pub(super) fn edit_process_canvas(&mut self, event: CanvasEvent) -> Task<Message> {
let context = event.context();
@@ -52,6 +49,47 @@ impl App {
}
return Task::none();
}
if let CanvasEvent::Camera(_, lifetime, input) = event {
use crate::behavior::canvas_workspace::{self, Event};
if lifetime != self.camera_epoch
|| !self.topology_context().is_some_and(|c| {
c.document == context.document
&& c.owner == context.owner
&& c.revision == context.revision
})
{
return Task::none();
}
let scope = canvas_workspace::Scope {
document: context.document,
owner: context.owner,
revision: context.revision,
epoch: lifetime,
};
if self.behavior.has_pending()
|| self.draft_dirty()
|| self.source_dirty()
|| !canvas_workspace::keyboard_enabled(self)
{
return Task::none();
}
match input {
Event::Focus
| Event::Traverse(_)
| Event::ControlFocus(_)
| Event::ToggleFocus
| Event::Leave => {
return canvas_workspace::focus_task(scope, input);
}
Event::CancelGesture => self.canvas_epoch = self.canvas_epoch.wrapping_add(1),
input => {
if self.behavior.topology.camera(input) {
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
}
}
}
return Task::none();
}
if self.topology_context() != Some(context) {
return Task::none();
}
@@ -81,6 +119,7 @@ impl App {
.first()
.copied()
.unwrap_or(endpoint.occurrence),
resolved.defining_sequence,
);
self.behavior.active_owner = Some(resolved.defining_sequence);
self.select(endpoint.occurrence);
@@ -118,14 +157,18 @@ impl App {
}
}
CanvasEvent::Choose(_, tool) => {
if self.behavior.has_pending() || !self.apply() {
if self.behavior.has_pending() || self.draft_dirty() || self.source_dirty() {
self.status =
"Apply or cancel pending edits before choosing a process tool.".into();
return Task::none();
}
self.behavior.topology.choose(tool);
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
}
CanvasEvent::Zoom(_, zoom) => {
self.behavior.topology.zoom(zoom);
let mut state = std::mem::take(&mut self.behavior.topology);
state.camera_zoom(self.project(), context.owner, zoom);
self.behavior.topology = state;
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
}
CanvasEvent::Expand(_, id) => {
@@ -149,7 +192,9 @@ impl App {
.get(&target)
.is_some_and(|e| matches!(e.kind(), Kind::Process | Kind::FunctionalChain))
{
self.behavior.topology.remember(context.owner, occurrence);
self.behavior
.topology
.remember(context.owner, occurrence, target);
self.behavior.active_owner = Some(target);
}
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
@@ -524,32 +569,30 @@ mod tests {
assert!(!app.behavior.has_pending());
}
#[test]
fn direct_process_app_save_scroll_resize_and_passive_geometry_keep_drafts_and_current_origin() {
fn direct_process_app_save_details_resize_and_passive_geometry_keep_drafts_and_camera() {
let (mut app, f) = setup();
let context = app.topology_context().unwrap();
let _ = app.update(Message::TopologyCanvas(CanvasEvent::Camera(
context,
app.camera_epoch,
behavior::canvas_workspace::Event::Pan(iced::Vector::new(25., 100.)),
)));
let context = app.topology_context().unwrap();
let original = app.document.snapshot();
app.busy = true;
for (area, offset) in [
(
behavior::scenario::ScrollArea::Canvas,
iced::Vector::new(25., 100.),
),
(
behavior::scenario::ScrollArea::Page,
iced::Vector::new(0., 40.),
),
] {
let _ = app.update(Message::TopologyCanvas(CanvasEvent::Scrolled(
context, area, offset,
)));
}
let _ = app.update(Message::TopologyCanvas(CanvasEvent::Scrolled(
context,
behavior::scenario::ScrollArea::Page,
iced::Vector::new(0., 40.),
)));
let _ = app.update(Message::TopologyCanvas(CanvasEvent::Viewport(
context,
iced::Size::new(500., 300.),
)));
let camera = app.behavior.topology.camera_boundary();
assert_eq!(
app.behavior.topology.scroll_offset(),
iced::Vector::new(25., 140.)
app.behavior.topology.details_offset(),
iced::Vector::new(0., 40.)
);
assert_ne!(app.topology_context(), Some(context));
assert!(app.busy);
@@ -557,12 +600,12 @@ mod tests {
stale.document = Id::new_v4();
let _ = app.update(Message::TopologyCanvas(CanvasEvent::Scrolled(
stale,
behavior::scenario::ScrollArea::Canvas,
behavior::scenario::ScrollArea::Page,
iced::Vector::new(0., 900.),
)));
assert_eq!(
app.behavior.topology.scroll_offset(),
iced::Vector::new(25., 140.)
app.behavior.topology.details_offset(),
iced::Vector::new(0., 40.)
);
let root = PathBuf::from("process-scroll-save-fixture");
let receipt = FileSnapshot {
@@ -582,6 +625,7 @@ mod tests {
assert!(!app.busy);
assert!(!app.document.can_undo());
assert_eq!(app.project(), &f.p);
assert!(app.behavior.topology.camera_boundary() == camera);
let context = app.topology_context().unwrap();
let size = app.window_size;
let _ = app.update(Message::WindowResized(size));
@@ -0,0 +1,304 @@
//! One transient chooser for the currently mounted Inspector or review field.
//! Browsing never calls a draft setter or semantic planner.
use super::{App, Message, Page, Pending};
use crate::{
forms::{
Draft,
reference_chooser::{self as chooser, Action, Close, Outcome, adapter, trigger},
},
navigation::InspectorTab,
semantic_review::{self, RowAddress, RowEvent},
workflow,
};
use iced::{Element, Rectangle, Task};
use oa_core::{FacetSelector, Id, Layer};
#[derive(Debug, Clone, PartialEq, Eq)]
enum Host {
Inspector,
Review {
address: Option<RowAddress>,
facet: FacetSelector,
},
}
impl Host {
fn message(&self, event: chooser::Event) -> Message {
match self {
Self::Inspector => Message::FieldReferenceEvent(event),
Self::Review { address, facet } => {
Message::Workflow(workflow::Event::semantic(semantic_review::Event::Row {
address: *address,
facet: Some(facet.clone()),
event: RowEvent::ReferenceEvent(event),
}))
}
}
}
}
struct Opened {
host: Host,
request: adapter::Open,
document_session: Id,
page: Page,
layer: Layer,
chooser: chooser::State,
}
#[derive(Default)]
pub(super) struct State {
opened: Option<Opened>,
}
impl App {
pub(crate) fn reference_input_active(&self) -> bool {
self.reference_authoring.opened.is_some()
}
fn reference_draft(&self, host: &Host, path: &str) -> Option<&Draft> {
match host {
Host::Inspector
if self.workflow.is_none() && self.inspector_tab == InspectorTab::Properties =>
{
self.draft.as_ref()
}
Host::Review { address, facet } => match self.workflow.as_ref()? {
workflow::Workflow::Semantic(state) => {
state.reference_draft(self.project(), *address, facet, path)
}
_ => None,
},
_ => None,
}
}
fn reference_available(&self) -> bool {
!self.busy
&& !self.hosted.visible
&& self.architecture.is_none()
&& self.model_menu.is_none()
&& self.file_prompt.is_none()
&& !self.delete_pending
&& self.pending.is_none()
&& !self.source_dirty()
&& !self.behavior.has_pending()
&& !matches!(self.page, Page::Home | Page::Guide | Page::Text)
}
fn reference_valid(&self, opened: &Opened) -> bool {
self.reference_available()
&& self.canvas_document == opened.document_session
&& self.page == opened.page
&& self.layer == opened.layer
&& self.document.revision() == opened.chooser.stamp().context.revision
&& self.project().id == opened.request.document
&& self
.reference_draft(&opened.host, &opened.request.path)
.is_some_and(|draft| draft.reference_matches(&opened.request))
}
pub(super) fn reconcile_reference_chooser(&mut self) {
if self
.reference_authoring
.opened
.as_ref()
.is_some_and(|opened| !self.reference_valid(opened))
{
self.reference_authoring
.opened
.as_mut()
.unwrap()
.chooser
.invalidate();
self.reference_authoring.opened = None;
}
}
fn open_reference(&mut self, host: Host, request: adapter::Open) -> Task<Message> {
if !self.reference_available() {
return Task::none();
}
let Some(draft) = self.reference_draft(&host, &request.path) else {
return Task::none();
};
let Ok(chooser) =
draft.reference_catalog(self.project(), &request, self.document.revision())
else {
return Task::none();
};
self.reference_authoring.opened = Some(Opened {
host,
request,
document_session: self.canvas_document,
page: self.page,
layer: self.layer,
chooser,
});
Task::none()
}
fn reference_event(&mut self, host: Host, event: chooser::Event) -> Task<Message> {
let Some(mut opened) = self.reference_authoring.opened.take() else {
return Task::none();
};
if !self.reference_valid(&opened) {
return Task::none();
}
if opened.host != host {
self.reference_authoring.opened = Some(opened);
return Task::none();
}
match opened.chooser.update(event) {
Outcome::Ignored | Outcome::Browsed => {
self.reference_authoring.opened = Some(opened);
Task::none()
}
Outcome::Closed(reason) => self.reference_focus(
&opened,
match reason {
Close::Escape => Some(0),
Close::Tab { backwards } => Some(if backwards { -1 } else { 1 }),
Close::Outside | Close::WindowChanged => None,
},
),
Outcome::Write { context, value } => {
let intent = adapter::Set {
context,
generation: opened.request.generation,
value,
};
let result = match &opened.host {
Host::Inspector => self
.draft
.as_mut()
.ok_or_else(|| "The property editor closed.".to_owned())
.and_then(|draft| draft.reference_set(self.document.project(), &intent)),
Host::Review { address, facet } => match self.workflow.as_mut() {
Some(workflow::Workflow::Semantic(state)) => {
state.set_reference(self.document.project(), *address, facet, intent)
}
_ => Err("The review editor closed.".into()),
},
};
if let Err(error) = result {
self.status = error;
}
// A scoped write rotates field generation. The task targets the
// newly rendered field, while a later structural edit makes it inert.
self.reference_focus(&opened, Some(0))
}
}
}
fn reference_focus(&self, opened: &Opened, step: Option<i32>) -> Task<Message> {
let Some(step) = step else {
return Task::none();
};
let Some(draft) = self.reference_draft(&opened.host, &opened.request.path) else {
return Task::none();
};
if draft.reference_session() != opened.request.session {
return Task::none();
}
trigger::focus(
draft.reference_session(),
draft.reference_generation(),
&opened.request.path,
step,
)
}
/// Called before generic modal dispatch: the review owns a modal, and this
/// chooser owns only its active field. All browser events stop here.
pub(super) fn reference_message(&mut self, message: &Message) -> Option<Task<Message>> {
self.reconcile_reference_chooser();
match message {
Message::FieldReferenceOpen(request) => {
Some(self.open_reference(Host::Inspector, request.clone()))
}
Message::FieldReferenceEvent(event) => {
Some(self.reference_event(Host::Inspector, event.clone()))
}
Message::Workflow(workflow::Event::Semantic(review_event)) => {
let semantic_review::Event::Row {
address,
facet: Some(facet),
event,
} = review_event.as_ref()
else {
return None;
};
let host = Host::Review {
address: *address,
facet: facet.clone(),
};
match event {
RowEvent::ReferenceOpen(request) => {
Some(self.open_reference(host, request.clone()))
}
RowEvent::ReferenceEvent(event) => {
Some(self.reference_event(host, event.clone()))
}
_ => None,
}
}
Message::Escape | Message::Request(Pending::Close(_))
if self.reference_authoring.opened.is_some() =>
{
let opened = self.reference_authoring.opened.as_ref().unwrap();
let host = opened.host.clone();
let event = opened.chooser.event(Action::Close(Close::Escape));
Some(self.reference_event(host, event))
}
// Global shortcuts are also emitted for keys captured by the search
// input. They must not apply drafts, navigate, or undo the model.
Message::Save(_)
| Message::ShortcutUndo(_)
| Message::Request(Pending::Open | Pending::New)
| Message::SourceNavigation(_)
| Message::Page(Page::Guide)
if self.reference_authoring.opened.is_some() =>
{
Some(Task::none())
}
_ => None,
}
}
pub(super) fn reference_overlay<
'a,
R: iced::advanced::text::Renderer<Font = iced::Font> + 'a,
>(
&'a self,
content: Element<'a, Message, iced::Theme, R>,
) -> Element<'a, Message, iced::Theme, R> {
let opened = self
.reference_authoring
.opened
.as_ref()
.filter(|opened| self.reference_valid(opened));
let host = opened
.map(|opened| opened.host.clone())
.unwrap_or(Host::Inspector);
let focus_scope = if self.reference_available() {
match self.workflow.as_ref() {
None if self.inspector_tab == InspectorTab::Properties => {
self.draft.as_ref().map(|draft| trigger::FormScope {
session: draft.reference_session(),
modal: false,
})
}
Some(workflow::Workflow::Semantic(state)) => state
.reference_form(self.project())
.map(|draft| trigger::FormScope {
session: draft.reference_session(),
modal: true,
}),
_ => None,
}
} else {
None
};
chooser::widget::Popup::new(
content,
opened.map(|opened| &opened.chooser),
opened.map_or(Rectangle::default(), |opened| opened.request.anchor),
move |event| host.message(event),
)
.focus_scope(focus_scope)
.into()
}
}
#[cfg(test)]
mod tests;
@@ -0,0 +1,586 @@
use super::*;
use crate::forms::Draft;
use oa_core::*;
use serde_json::{Value, json};
const OWNER: Id = Id::from_u128(100);
const BASE: u128 = 10_000;
const PATH: &str = "/body/Oa/Shared/DataType/definition/Record/supertype";
fn element(p: &mut Project, id: Id, kind: Kind, layer: Layer, name: &str, parent: Option<Id>) {
let generated = p.add_element(kind, layer, name, parent).unwrap();
let mut element = p.elements.remove(&generated).unwrap();
element.id = id;
element.alias = format!("object_{}", id.as_u128());
if let Some(SharedBody::DataType { definition, .. }) = element.shared_mut() {
*definition = DataTypeDefinition::Record { supertype: None };
}
p.elements.insert(id, element);
}
fn catalog() -> Project {
let mut p = Project::empty();
p.id = Id::from_u128(1);
p.name = "Reference catalog".into();
element(
&mut p,
OWNER,
Kind::DataType,
Layer::Oa,
"Edited record",
None,
);
for n in (0..1_000).rev() {
let id = Id::from_u128(BASE + n);
element(&mut p, id, Kind::DataType, Layer::Oa, "Controller", None);
p.elements.get_mut(&id).unwrap().alias = format!("controller_{n:04}");
}
element(
&mut p,
Id::from_u128(200),
Kind::DataType,
Layer::Sa,
"Controller",
None,
);
element(
&mut p,
Id::from_u128(201),
Kind::Activity,
Layer::Oa,
"Controller",
None,
);
p
}
fn inspector(p: Project, selected: Id) -> App {
let mut app = App::with_project(p);
let _ = app.update(Message::Select(selected));
app
}
fn request(app: &App, host: &Host, path: &str) -> adapter::Open {
app.reference_draft(host, path).unwrap().reference_open(
app.project(),
path,
Rectangle {
x: 790.,
y: 640.,
width: 278.,
height: 32.,
},
)
}
fn open(app: &mut App, host: &Host, path: &str) {
let request = request(app, host, path);
let message = match host {
Host::Inspector => Message::FieldReferenceOpen(request),
Host::Review { address, facet } => {
Message::Workflow(workflow::Event::semantic(semantic_review::Event::Row {
address: *address,
facet: Some(facet.clone()),
event: RowEvent::ReferenceOpen(request),
}))
}
};
let _ = app.update(message);
assert!(
app.reference_authoring.opened.is_some(),
"field must open: {}",
app.status
);
}
fn event(app: &App, action: Action) -> Message {
let opened = app.reference_authoring.opened.as_ref().unwrap();
opened.host.message(opened.chooser.event(action))
}
fn search(app: &mut App, query: &str) {
let serial = app
.reference_authoring
.opened
.as_ref()
.unwrap()
.chooser
.query_serial()
+ 1;
let event = event(
app,
Action::Query {
text: query.into(),
serial,
},
);
let _ = app.update(event);
}
fn choose(app: &mut App, id: Id) {
let event = event(app, Action::Choose(id));
let _ = app.update(event);
}
fn raw(app: &App) -> Value {
serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap()
}
fn review(app: &App) -> &semantic_review::State {
let Some(workflow::Workflow::Semantic(state)) = app.workflow.as_ref() else {
panic!("Review is open")
};
state
}
fn send_review(app: &mut App, event: RowEvent) {
let addressed = review(app).address_event(event);
let _ = app.update(Message::Workflow(workflow::Event::semantic(addressed)));
}
fn review_host(app: &App) -> Host {
let semantic_review::Event::Row {
address,
facet: Some(facet),
..
} = review(app).address_event(RowEvent::Preview)
else {
panic!("Active facet")
};
Host::Review { address, facet }
}
fn trace(p: &mut Project, id: Id, oa: Id, sa: Id) {
p.traces.insert(
id,
Trace {
id,
alias: format!("trace_{}", id.as_u128()),
name: "Rationale".into(),
description: String::new(),
oa,
sa,
kind: TraceKind::Manual,
baseline: None,
},
);
}
#[test]
fn inspector_reference_browse_is_read_only_then_one_scoped_value_applies_with_exact_undo_redo() {
let p = catalog();
let mut app = inspector(p.clone(), OWNER);
let before = app.document.bundle().clone();
let revision = app.document.revision();
// Malformed escaped text is unrelated to the selected reference. Browsing
// and the scoped write must keep its exact spelling and inactive payload.
app.draft.as_mut().unwrap().set_exact(true).unwrap();
let _ = app.update(Message::Field("/name".into(), "\"unfinished".into()));
let raw_before = raw(&app);
open(&mut app, &Host::Inspector, PATH);
assert_eq!(
app.reference_authoring
.opened
.as_ref()
.unwrap()
.chooser
.count(),
1_000
);
search(&mut app, "controller @controller_0720 OA");
let picker = &app.reference_authoring.opened.as_ref().unwrap().chooser;
assert_eq!(picker.count(), 1);
assert_eq!(picker.highlighted().unwrap().id, Id::from_u128(BASE + 720));
assert_eq!(raw(&app), raw_before);
assert_eq!(app.document.bundle(), &before);
assert_eq!(app.document.revision(), revision);
assert!(!app.document.can_undo());
// Captured search shortcuts cannot accidentally save/apply or undo a model.
let _ = app.update(Message::Save(false));
let _ = app.update(Message::ShortcutUndo(false));
assert_eq!(raw(&app), raw_before);
assert_eq!(app.document.bundle(), &before);
choose(&mut app, Id::from_u128(BASE + 720));
let mut expected_raw = raw_before;
*expected_raw["value"].pointer_mut(PATH).unwrap() = json!(Id::from_u128(BASE + 720));
expected_raw["inputs"][PATH] = json!(Id::from_u128(BASE + 720).to_string());
assert_eq!(raw(&app), expected_raw);
assert_eq!(app.document.bundle(), &before);
assert!(!app.document.can_undo());
let _ = app.update(Message::Apply);
assert_eq!(
app.document.bundle(),
&before,
"Unrelated invalid text remains an Apply error"
);
let _ = app.update(Message::Field("/name".into(), "\"Edited record\"".into()));
let _ = app.update(Message::Apply);
let mut expected = p;
if let Some(SharedBody::DataType { definition, .. }) =
expected.elements.get_mut(&OWNER).unwrap().shared_mut()
{
*definition = DataTypeDefinition::Record {
supertype: Some(Id::from_u128(BASE + 720)),
};
}
assert_eq!(app.project(), &expected);
let accepted = app.document.bundle().clone();
assert_ne!(accepted, before);
let _ = app.update(Message::Undo);
assert_eq!(app.document.bundle(), &before);
let _ = app.update(Message::Redo);
assert_eq!(app.document.bundle(), &accepted);
assert_eq!(app.project(), &expected);
}
#[test]
fn inspector_reference_retains_invalid_missing_and_incompatible_current_until_explicit_clear() {
for (raw_id, expected) in [
("-".into(), "invalid"),
(Id::from_u128(999).to_string(), "missing"),
(Id::from_u128(200).to_string(), "incompatible"),
] {
let mut app = inspector(catalog(), OWNER);
app.draft
.as_mut()
.unwrap()
.shape_scoped(PATH, json!(raw_id))
.unwrap();
let original = raw(&app);
let accepted = app.document.bundle().clone();
open(&mut app, &Host::Inspector, PATH);
let current = &app
.reference_authoring
.opened
.as_ref()
.unwrap()
.chooser
.current;
assert!(matches!(
(expected, current),
("invalid", chooser::Current::InvalidRaw(_))
| ("missing", chooser::Current::Missing(_))
| ("incompatible", chooser::Current::Incompatible(_))
));
let _ = app.update(Message::Escape);
assert_eq!(raw(&app), original);
open(&mut app, &Host::Inspector, PATH);
let clear = event(&app, Action::Clear);
let _ = app.update(clear);
assert!(
app.draft
.as_ref()
.unwrap()
.value
.pointer(PATH)
.unwrap()
.is_null()
);
assert_eq!(app.document.bundle(), &accepted);
assert!(!app.document.can_undo());
}
}
#[test]
fn chooser_rejects_old_results_after_context_recovery_reload_or_modal_and_keeps_history_text_out() {
for change in 0..7 {
let mut app = inspector(catalog(), OWNER);
open(&mut app, &Host::Inspector, PATH);
let delayed = event(&app, Action::Choose(Id::from_u128(BASE + 1)));
match change {
0 => {
app.draft
.as_mut()
.unwrap()
.shape_scoped(PATH, json!(Id::from_u128(BASE + 2)))
.unwrap();
}
1 => {
let saved = app.draft.as_ref().unwrap().recovery_snapshot();
app.draft = Some(Draft::from_recovery(app.project(), saved).unwrap());
}
2 => {
app.canvas_document = Id::new_v4();
}
3 => {
app.hosted.visible = true;
}
4 => {
app.draft
.as_mut()
.unwrap()
.set_context(Some(json!({"different":"scope"})));
}
5 => {
let _ = app.update(Message::Command(Command::Rename(
Id::from_u128(BASE + 3),
"Changed model".into(),
)));
}
_ => {
app.source_workspace.edit(
"unfinished source".into(),
iced::widget::text_editor::Cursor {
position: iced::widget::text_editor::Position { line: 0, column: 0 },
selection: None,
},
);
}
}
let expected = raw(&app);
let source = app.document.bundle().clone();
let _ = app.update(delayed);
assert!(app.reference_authoring.opened.is_none());
assert_eq!(raw(&app), expected);
assert_eq!(app.document.bundle(), &source);
}
let mut p = catalog();
trace(&mut p, Id::from_u128(300), OWNER, Id::from_u128(200));
let app = inspector(p, Id::from_u128(300));
let draft = app.draft.as_ref().unwrap();
let request = draft.reference_open(
app.project(),
"/baseline/semantic/facets/0/references/0/target",
Rectangle::default(),
);
assert!(draft.reference_catalog(app.project(), &request, 0).is_err());
}
#[test]
fn reference_array_row_reorder_and_constructor_switch_invalidate_exact_old_field() {
let mut p = Project::empty();
for n in 0..3 {
element(
&mut p,
Id::from_u128(BASE + n),
Kind::ExchangeItem,
Layer::Oa,
"Signal item",
None,
);
}
element(&mut p, OWNER, Kind::Event, Layer::Oa, "Signal", None);
*p.elements.get_mut(&OWNER).unwrap().shared_mut().unwrap() = SharedBody::Event {
body: EventBody::Signal {
items: vec![Id::from_u128(BASE), Id::from_u128(BASE + 1)],
},
};
let mut app = inspector(p, OWNER);
let list = "/body/Oa/Shared/Event/body/Signal/items";
let path = format!("{list}/0");
open(&mut app, &Host::Inspector, &path);
let delayed = event(&app, Action::Choose(Id::from_u128(BASE + 2)));
let _ = app.update(Message::FieldArray(list.into(), Some(0), 1));
let expected = raw(&app);
assert_eq!(
app.draft.as_ref().unwrap().value.pointer(list),
Some(&json!([Id::from_u128(BASE + 1), Id::from_u128(BASE)]))
);
let _ = app.update(delayed);
assert_eq!(raw(&app), expected);
open(&mut app, &Host::Inspector, &path);
let delayed = event(&app, Action::Choose(Id::from_u128(BASE + 2)));
let _ = app.update(Message::FieldVariant(
"/body/Oa/Shared/Event/body".into(),
json!({"Change":{"expression":"unfinished"}}),
));
let expected = raw(&app);
let _ = app.update(delayed);
assert_eq!(raw(&app), expected);
assert!(!app.document.can_undo());
}
#[test]
fn review_reference_facets_groups_and_attachment_hosts_browse_without_replanning_and_reject_old_facet()
{
for mode in 0..3 {
let mut p = Project::empty();
p.id = Id::from_u128(1);
for (id, layer, parent) in [
(100, Layer::Oa, None),
(101, Layer::Sa, None),
(102, Layer::Sa, None),
(103, Layer::Oa, Some(Id::from_u128(100))),
(104, Layer::Sa, Some(Id::from_u128(101))),
] {
element(
&mut p,
Id::from_u128(id),
if parent.is_some() {
Kind::DataField
} else {
Kind::DataType
},
layer,
"Record",
parent,
);
}
trace(
&mut p,
Id::from_u128(300),
Id::from_u128(100),
Id::from_u128(101),
);
let facet = match mode {
0 => FacetSelector::Data(DataFacet::RecordSupertype),
1 => FacetSelector::Data(DataFacet::Constructor),
_ => FacetSelector::Child(KeyPair {
oa: Some(Id::from_u128(103)),
sa: Some(Id::from_u128(104)),
}),
};
let path = if mode == 2 {
"/parent"
} else {
"/body/Sa/Shared/DataType/definition/Record/supertype"
};
let mut app = App::with_project(p.clone());
let _ = app.update(Message::StartSemanticReview(Id::from_u128(100)));
if mode == 2 {
// A paired attachment is a real catalog facet only after explicit
// correspondence; selecting an unstaged synthetic pair is rejected.
for (id, oa) in [(103, true), (104, false)] {
let choice = crate::app::Choice {
id: Id::from_u128(id),
label: format!("Record · @object_{id}"),
};
send_review(
&mut app,
if oa {
RowEvent::ReferenceOa(choice)
} else {
RowEvent::ReferenceSa(choice)
},
);
}
send_review(&mut app, RowEvent::AddReference);
}
send_review(&mut app, RowEvent::Select(facet.clone()));
send_review(
&mut app,
RowEvent::Decide(facet.clone(), Some(SemanticDecision::Candidate)),
);
let host = review_host(&app);
let record = if mode == 2 {
Id::from_u128(104)
} else {
Id::from_u128(101)
};
assert_eq!(
app.reference_draft(&host, path)
.unwrap_or_else(|| {
panic!(
"Reference fixture mode={mode}, facet={facet:?}, selected={:?}, decisions={:?}, error={:?}",
review(&app).selected,
review(&app).decisions,
review(&app).error
)
})
.original,
record
);
let before = review(&app).recovery_snapshot().unwrap();
let plan = serde_json::to_value(&review(&app).plan).unwrap();
let proposed = review(&app).proposed.clone();
let source = app.document.bundle().clone();
open(&mut app, &host, path);
search(&mut app, "@object_102 SA");
assert_eq!(
app.reference_authoring
.opened
.as_ref()
.unwrap()
.chooser
.count(),
1
);
assert_eq!(review(&app).recovery_snapshot().unwrap(), before);
assert_eq!(serde_json::to_value(&review(&app).plan).unwrap(), plan);
assert_eq!(review(&app).proposed, proposed);
assert_eq!(app.document.bundle(), &source);
let old = event(&app, Action::Choose(Id::from_u128(102)));
send_review(
&mut app,
RowEvent::Select(FacetSelector::Header(HeaderFacet::Name)),
);
let switched = review(&app).recovery_snapshot().unwrap();
let _ = app.update(old);
assert_eq!(review(&app).recovery_snapshot().unwrap(), switched);
send_review(&mut app, RowEvent::Select(facet));
open(&mut app, &host, path);
choose(&mut app, Id::from_u128(102));
let draft = app.reference_draft(&host, path).unwrap();
let mut expected_record = serde_json::to_value(&p.elements[&record]).unwrap();
*expected_record.pointer_mut(path).unwrap() = json!(Id::from_u128(102));
assert_eq!(draft.materialize().unwrap(), expected_record);
assert_eq!(app.document.bundle(), &source);
assert!(
!app.document.can_undo(),
"A choice only stages the active review candidate"
);
}
}
mod mounted;
#[test]
fn review_reference_cannot_reopen_or_write_a_banked_row_after_another_row_takes_focus() {
let mut p = Project::empty();
for (id, layer) in [
(100, Layer::Oa),
(101, Layer::Sa),
(102, Layer::Sa),
(110, Layer::Oa),
(111, Layer::Sa),
] {
element(
&mut p,
Id::from_u128(id),
Kind::DataType,
layer,
"Record",
None,
);
}
trace(
&mut p,
Id::from_u128(300),
Id::from_u128(100),
Id::from_u128(101),
);
trace(
&mut p,
Id::from_u128(310),
Id::from_u128(110),
Id::from_u128(111),
);
let mut app = App::with_project(p);
let _ = app.update(Message::StartSemanticReview(Id::from_u128(300)));
let facet = FacetSelector::Data(DataFacet::RecordSupertype);
send_review(&mut app, RowEvent::Select(facet.clone()));
send_review(
&mut app,
RowEvent::Decide(facet, Some(SemanticDecision::Candidate)),
);
let host = review_host(&app);
let path = "/body/Sa/Shared/DataType/definition/Record/supertype";
let old_open = request(&app, &host, path);
open(&mut app, &host, path);
let old_choice = event(&app, Action::Choose(Id::from_u128(102)));
for event in [
semantic_review::Event::ChooseTrace(crate::app::Choice {
id: Id::from_u128(310),
label: "Second exact trace".into(),
}),
semantic_review::Event::OpenTrace,
] {
let _ = app.update(Message::Workflow(workflow::Event::semantic(event)));
}
assert_eq!(review(&app).target(app.project()), Some(Id::from_u128(111)));
let expected = review(&app).recovery_snapshot().unwrap();
let source = app.document.bundle().clone();
let _ = app.update(old_choice);
let Host::Review { address, facet } = host else {
unreachable!()
};
let _ = app.update(Message::Workflow(workflow::Event::semantic(
semantic_review::Event::Row {
address,
facet: Some(facet),
event: RowEvent::ReferenceOpen(old_open),
},
)));
assert!(app.reference_authoring.opened.is_none());
assert_eq!(review(&app).recovery_snapshot().unwrap(), expected);
assert_eq!(app.document.bundle(), &source);
assert!(!app.document.can_undo());
}
@@ -0,0 +1,910 @@
//! Real field + app-root overlay interaction. The null text renderer lets the
//! same retained widget tree exercise focus and geometry without a GPU/window.
use super::*;
use iced::futures::StreamExt;
use iced::{
Element, Event as Input, Point, Size, Theme, Vector,
advanced::{
Layout, Shell, layout,
widget::{Id as WidgetId, Operation, Tree, operation},
},
keyboard, mouse,
widget::{Space, column, container, opaque, scrollable, stack, text_input},
};
use oa_core::transitions::TransitionDirection;
#[derive(Default)]
struct Clipboard;
impl iced::advanced::Clipboard for Clipboard {
fn read(&self, _: iced::advanced::clipboard::Kind) -> Option<String> {
None
}
fn write(&mut self, _: iced::advanced::clipboard::Kind, _: String) {}
}
#[derive(Default)]
struct Fields {
values: Vec<(WidgetId, Rectangle, bool)>,
scrolls: Vec<(Rectangle, Vector)>,
}
impl Operation for Fields {
fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn Operation)) {
visit(self);
}
fn focusable(
&mut self,
id: Option<&WidgetId>,
bounds: Rectangle,
state: &mut dyn operation::Focusable,
) {
if let Some(id) = id {
self.values.push((id.clone(), bounds, state.is_focused()));
}
}
fn scrollable(
&mut self,
_: Option<&WidgetId>,
bounds: Rectangle,
_: Rectangle,
translation: Vector,
_: &mut dyn operation::Scrollable,
) {
self.scrolls.push((bounds, translation));
}
}
impl Fields {
fn field(&self, id: &WidgetId) -> (Rectangle, bool) {
self.values
.iter()
.find(|(actual, _, _)| actual == id)
.map(|(_, bounds, focused)| (*bounds, *focused))
.expect("Mounted field identity")
}
}
fn view(
app: &App,
scrolled: bool,
nested: bool,
show_review: bool,
) -> Element<'_, Message, Theme, ()> {
let draft = app.draft.as_ref().unwrap();
let form = column![
text_input("Name", &draft.inputs["/name"].raw)
.id("before-reference")
.on_input(|raw| Message::Field("/name".into(), raw)),
adapter::field(draft, app.project(), PATH),
text_input("Description", &draft.inputs["/description"].raw)
.id("after-reference")
.on_input(|raw| Message::Field("/description".into(), raw)),
]
.spacing(9);
let form = trigger::scope(form, draft.reference_session());
let base: Element<'_, Message, Theme, ()> = if scrolled {
container(
scrollable(column![Space::new().height(500), form])
.id("reference-scroll")
.height(180),
)
.width(300)
.into()
} else {
container(form).width(300).height(iced::Fill).into()
};
let base: Element<'_, Message, Theme, ()> = if nested {
scrollable(column![Space::new().height(400), base])
.id("outer-reference-scroll")
.height(180)
.width(300)
.into()
} else {
base
};
let base = if show_review {
let review: Element<'_, Message, Theme, ()> = match app.workflow.as_ref() {
Some(workflow::Workflow::Semantic(state)) => {
match state.reference_form(app.project()) {
Some(draft) => {
let mut content = column![];
if let Some(raw) = &state.operation_omission {
content = content.push(
text_input("Required engineering rationale", raw)
.id("review-rationale")
.on_input(|raw| {
Message::Workflow(workflow::Event::semantic(
state.address_event(RowEvent::OperationRationale(raw)),
))
}),
);
}
content = content.push(trigger::scope(
adapter::field(draft, app.project(), "/parent").map(|message| {
match message {
Message::FieldReferenceOpen(request) => {
Message::Workflow(workflow::Event::semantic(
state.address_event(RowEvent::ReferenceOpen(request)),
))
}
other => other,
}
}),
draft.reference_session(),
));
opaque(container(trigger::modal(content)).width(300).height(180))
}
None => Space::new().into(),
}
}
_ => Space::new().into(),
};
stack![base, review].into()
} else {
base
};
app.reference_overlay(base)
}
struct Mount {
tree: Option<Tree>,
size: Size,
scrolled: bool,
nested: bool,
show_review: bool,
}
impl Default for Mount {
fn default() -> Self {
Self {
tree: None,
size: Size::new(360., 320.),
scrolled: false,
nested: false,
show_review: false,
}
}
}
impl Mount {
fn frame(
&mut self,
app: &App,
events: &[(Input, Option<Point>)],
) -> (Vec<Message>, Fields, Option<Rectangle>) {
let mut view = view(app, self.scrolled, self.nested, self.show_review);
let tree = self.tree.get_or_insert_with(|| Tree::new(view.as_widget()));
view.as_widget().diff(tree);
let node =
view.as_widget_mut()
.layout(tree, &(), &layout::Limits::new(Size::ZERO, self.size));
let viewport = Rectangle::with_size(self.size);
let overlay_node = view
.as_widget_mut()
.overlay(tree, Layout::new(&node), &(), &viewport, Vector::ZERO)
.map(|mut overlay| overlay.as_overlay_mut().layout(&(), self.size));
let mut messages = Vec::new();
for (input, point) in events {
let mut shell = Shell::new(&mut messages);
let cursor = point.map_or(mouse::Cursor::Unavailable, mouse::Cursor::Available);
if let Some(overlay_node) = &overlay_node {
let mut overlay = view
.as_widget_mut()
.overlay(tree, Layout::new(&node), &(), &viewport, Vector::ZERO)
.unwrap();
overlay.as_overlay_mut().update(
input,
Layout::new(overlay_node),
cursor,
&(),
&mut Clipboard,
&mut shell,
);
assert!(shell.is_event_captured());
} else {
view.as_widget_mut().update(
tree,
input,
Layout::new(&node),
cursor,
&(),
&mut Clipboard,
&mut shell,
&viewport,
);
}
}
let mut fields = Fields::default();
view.as_widget_mut()
.operate(tree, Layout::new(&node), &(), &mut fields);
(
messages,
fields,
overlay_node.map(|node| node.children()[0].bounds()),
)
}
fn click(&mut self, app: &App, point: Point) -> Vec<Message> {
self.frame(
app,
&[
(
Input::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
Some(point),
),
(
Input::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)),
Some(point),
),
],
)
.0
}
async fn perform(&mut self, app: &App, task: Task<Message>) {
let Some(mut stream) = iced_runtime::task::into_stream(task) else {
return;
};
while let Some(action) = stream.next().await {
let iced_runtime::Action::Widget(mut operation) = action else {
panic!("Chooser only requests scoped widget focus")
};
let mut view = view(app, self.scrolled, self.nested, self.show_review);
let tree = self.tree.get_or_insert_with(|| Tree::new(view.as_widget()));
view.as_widget().diff(tree);
let node =
view.as_widget_mut()
.layout(tree, &(), &layout::Limits::new(Size::ZERO, self.size));
loop {
view.as_widget_mut()
.operate(tree, Layout::new(&node), &(), operation.as_mut());
match operation.finish() {
operation::Outcome::Chain(next) => operation = next,
_ => break,
}
}
}
}
async fn dispatch(&mut self, app: &mut App, messages: Vec<Message>) {
for message in messages {
let task = app.update(message);
self.perform(app, task).await;
}
}
}
fn key(key: keyboard::key::Named, shift: bool) -> Input {
Input::Keyboard(keyboard::Event::KeyPressed {
key: keyboard::Key::Named(key),
modified_key: keyboard::Key::Named(key),
physical_key: keyboard::key::Physical::Code(keyboard::key::Code::Space),
location: keyboard::Location::Standard,
modifiers: if shift {
keyboard::Modifiers::SHIFT
} else {
keyboard::Modifiers::empty()
},
text: None,
repeat: false,
})
}
fn typing(raw: &str) -> Vec<(Input, Option<Point>)> {
raw.chars()
.map(|character| {
let raw = character.to_string();
(
Input::Keyboard(keyboard::Event::KeyPressed {
key: keyboard::Key::Character(raw.clone().into()),
modified_key: keyboard::Key::Character(raw.clone().into()),
physical_key: keyboard::key::Physical::Code(keyboard::key::Code::KeyA),
location: keyboard::Location::Standard,
modifiers: keyboard::Modifiers::empty(),
text: Some(raw.into()),
repeat: false,
}),
None,
)
})
.collect()
}
#[tokio::test]
async fn mounted_reference_trigger_tabs_to_root_popup_search_escape_and_exact_adjacent_fields() {
let mut app = inspector(catalog(), OWNER);
let accepted = app.document.bundle().clone();
let original = raw(&app);
let mut mount = Mount::default();
let fields = mount.frame(&app, &[]).1;
let messages = mount.click(
&app,
fields.field(&WidgetId::new("before-reference")).0.center(),
);
mount.dispatch(&mut app, messages).await;
let (_, fields, _) = mount.frame(&app, &[(key(keyboard::key::Named::Tab, false), None)]);
let id = trigger::field_id(app.draft.as_ref().unwrap().reference_generation(), PATH);
assert!(
fields.field(&id).1,
"Tab reaches the real reference trigger"
);
let messages = mount
.frame(&app, &[(key(keyboard::key::Named::Enter, false), None)])
.0;
assert_eq!(messages.len(), 1, "One keyboard activation opens one field");
mount.dispatch(&mut app, messages).await;
let popup = mount
.frame(&app, &[])
.2
.expect("App root overlay is mounted");
assert!(
popup.x >= 0.
&& popup.y >= 0.
&& popup.x + popup.width <= 360.
&& popup.y + popup.height <= 320.
);
let mut inputs = typing("@controller_0720");
inputs.push((key(keyboard::key::Named::Escape, false), None));
let messages = mount.frame(&app, &inputs).0;
mount.dispatch(&mut app, messages).await;
let (_, fields, popup) = mount.frame(&app, &[]);
assert!(popup.is_none());
assert!(
fields.field(&id).1,
"Escape returns to its exact reference field"
);
assert_eq!(raw(&app), original);
assert_eq!(app.document.bundle(), &accepted);
for (backwards, expected) in [(false, "after-reference"), (true, "before-reference")] {
let messages = mount.click(&app, fields.field(&id).0.center());
mount.dispatch(&mut app, messages).await;
let messages = mount
.frame(&app, &[(key(keyboard::key::Named::Tab, backwards), None)])
.0;
mount.dispatch(&mut app, messages).await;
let (_, fields, popup) = mount.frame(&app, &[]);
assert!(popup.is_none());
assert!(
fields.field(&WidgetId::new(expected)).1,
"Chooser Tab stays within the originating form"
);
}
assert_eq!(app.document.bundle(), &accepted);
}
#[tokio::test]
async fn mounted_reference_choice_stages_literal_identity_and_delayed_focus_cannot_hit_replacement_field()
{
let mut app = inspector(catalog(), OWNER);
let accepted = app.document.bundle().clone();
let mut mount = Mount::default();
let id = trigger::field_id(app.draft.as_ref().unwrap().reference_generation(), PATH);
let fields = mount.frame(&app, &[]).1;
let messages = mount.click(&app, fields.field(&id).0.center());
mount.dispatch(&mut app, messages).await;
let messages = mount.frame(&app, &typing("@controller_0720")).0;
mount.dispatch(&mut app, messages).await;
assert_eq!(
app.reference_authoring
.opened
.as_ref()
.unwrap()
.chooser
.count(),
1
);
let messages = mount
.frame(&app, &[(key(keyboard::key::Named::Enter, false), None)])
.0;
assert_eq!(messages.len(), 1);
let delayed_focus = app.update(messages.into_iter().next().unwrap());
assert_eq!(
app.draft.as_ref().unwrap().value.pointer(PATH),
Some(&json!(Id::from_u128(BASE + 720)))
);
assert_eq!(app.document.bundle(), &accepted);
// A structural edit occurs after Choose but before its asynchronous focus.
app.draft
.as_mut()
.unwrap()
.shape_scoped(PATH, Value::Null)
.unwrap();
let fields = mount.frame(&app, &[]).1;
let messages = mount.click(
&app,
fields.field(&WidgetId::new("after-reference")).0.center(),
);
mount.dispatch(&mut app, messages).await;
mount.perform(&app, delayed_focus).await;
let fields = mount.frame(&app, &[]).1;
assert!(
fields.field(&WidgetId::new("after-reference")).1,
"A missing old field target makes no focus changes"
);
assert!(
!fields
.field(&trigger::field_id(
app.draft.as_ref().unwrap().reference_generation(),
PATH
))
.1
);
assert_eq!(app.document.bundle(), &accepted);
}
#[tokio::test]
async fn mounted_scrolled_reference_anchor_uses_window_geometry_for_pointer_and_keyboard() {
let mut app = inspector(catalog(), OWNER);
let mut mount = Mount {
scrolled: true,
..Default::default()
};
mount.frame(&app, &[]);
mount
.perform(
&app,
iced::widget::operation::snap_to("reference-scroll", scrollable::RelativeOffset::END),
)
.await;
let fields = mount.frame(&app, &[]).1;
let id = trigger::field_id(app.draft.as_ref().unwrap().reference_generation(), PATH);
let content = fields.field(&id).0;
let (viewport, offset) = fields.scrolls[0];
assert!(offset.y > 400., "Fixture is genuinely scrolled");
let expected = Rectangle {
x: content.x - offset.x,
y: content.y - offset.y,
..content
}
.intersection(&viewport)
.unwrap();
let messages = mount.click(&app, expected.center());
let [Message::FieldReferenceOpen(request)] = messages.as_slice() else {
panic!("Scrolled field should emit one actual Open")
};
assert_eq!(request.anchor, expected);
mount.dispatch(&mut app, messages).await;
let close = app.update(Message::Escape);
mount.perform(&app, close).await;
let messages = mount
.frame(&app, &[(key(keyboard::key::Named::Enter, false), None)])
.0;
let [Message::FieldReferenceOpen(request)] = messages.as_slice() else {
panic!("Focused scrolled field opens without a pointer")
};
assert_eq!(request.anchor, expected);
}
#[tokio::test]
async fn mounted_fresh_tab_reveals_offscreen_reference_and_return_reveals_it_again() {
for nested in [false, true] {
let mut app = inspector(catalog(), OWNER);
let original = raw(&app);
let source = app.document.bundle().clone();
let mut mount = Mount {
scrolled: true,
nested,
..Default::default()
};
let fields = mount.frame(&app, &[]).1;
assert!(
fields
.scrolls
.iter()
.all(|(_, offset)| *offset == Vector::ZERO)
);
assert!(fields.field(&WidgetId::new("before-reference")).0.y > 180.);
let messages = mount
.frame(&app, &[(key(keyboard::key::Named::Tab, false), None)])
.0;
assert!(
messages.is_empty(),
"Keyboard reveal must not author any input"
);
let fields = mount.frame(&app, &[]).1;
assert!(fields.field(&WidgetId::new("before-reference")).1);
assert!(
fields.scrolls.iter().all(|(_, offset)| offset.y > 0.),
"Every enclosing viewport reveals fresh focus"
);
let id = trigger::field_id(app.draft.as_ref().unwrap().reference_generation(), PATH);
let offset: f32 = fields.scrolls.iter().map(|(_, offset)| offset.y).sum();
assert!(
fields.field(&id).0.y - offset >= 180.,
"The next reference starts below the fold"
);
let messages = mount
.frame(&app, &[(key(keyboard::key::Named::Tab, false), None)])
.0;
assert!(messages.is_empty());
let fields = mount.frame(&app, &[]).1;
let offset: f32 = fields.scrolls.iter().map(|(_, offset)| offset.y).sum();
let (bounds, focused) = fields.field(&id);
assert!(focused);
assert!(bounds.y - offset >= -0.01 && bounds.y + bounds.height - offset <= 180.01);
let messages = mount
.frame(&app, &[(key(keyboard::key::Named::Enter, false), None)])
.0;
assert_eq!(
messages.len(),
1,
"A newly revealed field opens through Enter"
);
mount.dispatch(&mut app, messages).await;
let return_focus = app.update(Message::Escape);
mount
.perform(
&app,
iced::widget::operation::snap_to(
"reference-scroll",
scrollable::RelativeOffset::START,
),
)
.await;
if nested {
mount
.perform(
&app,
iced::widget::operation::snap_to(
"outer-reference-scroll",
scrollable::RelativeOffset::START,
),
)
.await;
}
assert!(
mount
.frame(&app, &[])
.1
.scrolls
.iter()
.all(|(_, offset)| *offset == Vector::ZERO)
);
mount.perform(&app, return_focus).await;
let fields = mount.frame(&app, &[]).1;
let offset: f32 = fields.scrolls.iter().map(|(_, offset)| offset.y).sum();
let (bounds, focused) = fields.field(&id);
assert!(
focused && bounds.y - offset >= -0.01 && bounds.y + bounds.height - offset <= 180.01
);
assert_eq!(raw(&app), original);
assert_eq!(app.document.bundle(), &source);
}
}
#[tokio::test]
async fn mounted_form_tab_enters_in_both_directions_and_cycles_boundaries_without_losing_focus() {
for backwards in [false, true] {
let app = inspector(catalog(), OWNER);
let original = raw(&app);
let mut mount = Mount::default();
let reference = trigger::field_id(app.draft.as_ref().unwrap().reference_generation(), PATH);
let forward = [
WidgetId::new("before-reference"),
reference.clone(),
WidgetId::new("after-reference"),
];
let reverse = [
WidgetId::new("after-reference"),
reference,
WidgetId::new("before-reference"),
];
let expected = if backwards { &reverse } else { &forward };
for n in 0..7 {
let (messages, fields, popup) =
mount.frame(&app, &[(key(keyboard::key::Named::Tab, backwards), None)]);
assert!(messages.is_empty() && popup.is_none());
assert!(fields.field(&expected[n % 3]).1);
assert_eq!(
fields
.values
.iter()
.filter(|(_, _, focused)| *focused)
.count(),
1
);
}
assert_eq!(raw(&app), original);
assert!(!app.document.can_undo());
}
}
#[tokio::test]
async fn mounted_delayed_return_cannot_focus_under_review_and_fresh_modal_tab_owns_its_form() {
let mut p = catalog();
trace(&mut p, Id::from_u128(300), OWNER, Id::from_u128(200));
let mut app = inspector(p, OWNER);
let source = app.document.bundle().clone();
let original = raw(&app);
let mut mount = Mount {
show_review: true,
..Default::default()
};
let id = trigger::field_id(app.draft.as_ref().unwrap().reference_generation(), PATH);
let session = app.draft.as_ref().unwrap().reference_session();
let fields = mount.frame(&app, &[]).1;
let messages = mount.click(&app, fields.field(&id).0.center());
mount.dispatch(&mut app, messages).await;
let return_focus = app.update(Message::Escape);
let fields = mount.frame(&app, &[]).1;
let messages = mount.click(
&app,
fields.field(&WidgetId::new("after-reference")).0.center(),
);
mount.dispatch(&mut app, messages).await;
let _ = app.update(Message::StartSemanticReview(Id::from_u128(300)));
let facet = FacetSelector::Header(HeaderFacet::Parent);
send_review(&mut app, RowEvent::Select(facet.clone()));
send_review(
&mut app,
RowEvent::Decide(facet, Some(SemanticDecision::Candidate)),
);
assert_eq!(
app.draft.as_ref().unwrap().reference_session(),
session,
"Old Inspector really remains mounted"
);
assert_eq!(
trigger::field_id(app.draft.as_ref().unwrap().reference_generation(), PATH),
id
);
let review_before = review(&app).recovery_snapshot().unwrap();
let review_field = trigger::field_id(
review(&app)
.reference_form(app.project())
.unwrap()
.reference_generation(),
"/parent",
);
mount.perform(&app, return_focus).await;
let fields = mount.frame(&app, &[]).1;
assert!(
!fields.field(&id).1,
"A live old field underneath a modal cannot receive the stale focus task"
);
assert!(fields.field(&WidgetId::new("after-reference")).1);
assert!(!fields.field(&review_field).1);
let (messages, fields, _) = mount.frame(&app, &[(key(keyboard::key::Named::Tab, false), None)]);
assert!(messages.is_empty());
assert!(
fields.field(&review_field).1,
"Review entry ignores retained focus under its modal"
);
assert!(
!fields.field(&WidgetId::new("after-reference")).1,
"Modal focus acquisition deactivates background inputs"
);
let messages = mount.frame(&app, &typing("x")).0;
assert!(
messages.is_empty(),
"A character at the focused Review trigger cannot type into the Inspector below it"
);
let (messages, fields, _) = mount.frame(&app, &[(key(keyboard::key::Named::Tab, true), None)]);
assert!(messages.is_empty());
assert!(fields.field(&review_field).1);
assert!(!fields.field(&id).1);
assert_eq!(review(&app).recovery_snapshot().unwrap(), review_before);
assert_eq!(raw(&app), original);
assert_eq!(app.document.bundle(), &source);
}
#[tokio::test]
async fn mounted_review_pointer_open_and_outside_dismissal_keep_background_input_inactive() {
let mut p = catalog();
trace(&mut p, Id::from_u128(300), OWNER, Id::from_u128(200));
let mut app = inspector(p, OWNER);
let original = raw(&app);
let source = app.document.bundle().clone();
let mut mount = Mount {
show_review: true,
..Default::default()
};
let fields = mount.frame(&app, &[]).1;
let messages = mount.click(
&app,
fields.field(&WidgetId::new("after-reference")).0.center(),
);
mount.dispatch(&mut app, messages).await;
let _ = app.update(Message::StartSemanticReview(Id::from_u128(300)));
let facet = FacetSelector::Header(HeaderFacet::Parent);
send_review(&mut app, RowEvent::Select(facet.clone()));
send_review(
&mut app,
RowEvent::Decide(facet, Some(SemanticDecision::Candidate)),
);
let saved = review(&app).recovery_snapshot().unwrap();
let id = trigger::field_id(
review(&app)
.reference_form(app.project())
.unwrap()
.reference_generation(),
"/parent",
);
let fields = mount.frame(&app, &[]).1;
assert!(
fields.field(&WidgetId::new("after-reference")).1,
"Fixture retains Inspector focus before the modal receives input"
);
let messages = mount.click(&app, fields.field(&id).0.center());
mount.dispatch(&mut app, messages).await;
assert!(matches!(
&app.reference_authoring.opened.as_ref().unwrap().host,
Host::Review { .. }
));
let fields = mount.frame(&app, &[]).1;
assert!(!fields.field(&WidgetId::new("after-reference")).1);
let messages = mount.click(&app, Point::new(359., 319.));
assert!(matches!(
messages.as_slice(),
[Message::Workflow(workflow::Event::Semantic(event))] if matches!(event.as_ref(),
semantic_review::Event::Row {
event: RowEvent::ReferenceEvent(chooser::Event {
action: Action::Close(Close::Outside),
..
}),
..
}
)
));
for message in messages {
let task = app.update(message);
assert!(
iced_runtime::task::into_stream(task).is_none(),
"Outside dismissal must not request focus or apply"
);
}
let (messages, fields, popup) = mount.frame(&app, &typing("x"));
assert!(messages.is_empty() && popup.is_none());
assert!(!fields.field(&WidgetId::new("after-reference")).1);
assert_eq!(review(&app).recovery_snapshot().unwrap(), saved);
assert_eq!(raw(&app), original);
assert_eq!(app.document.bundle(), &source);
}
#[tokio::test]
async fn mounted_reverse_review_required_rationale_remains_editable_beside_candidate_reference_form()
{
let mut p = persistence::decode(include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../harness/fixtures/interface-operation-probe/output/boundary-only.json"
)))
.unwrap();
let sa = p
.elements
.values()
.find(|element| element.alias == "operation_probe_43")
.unwrap()
.id;
let allocation = p
.relations
.values()
.find(|relation| relation.alias == "operation_probe_30")
.unwrap()
.id;
// The boundary fixture deliberately has no selected operation. Author one
// through the validated command path before testing reverse omission.
let mut message = p.elements[&sa].clone();
let Some(SharedBody::Occurrence {
body: OccurrenceBody::Exchange { operation, .. },
..
}) = message.shared_mut()
else {
panic!("The operation fixture must contain an Exchange call")
};
*operation = Some(OperationUse {
allocation: Some(allocation),
});
p = execute_batch(&p, &[Command::EditElement(sa, message)]).unwrap();
assert!(matches!(
oa_core::operations::resolve(&p, sa).unwrap(),
oa_core::operations::OperationResolution::Selected(value)
if value.allocation == allocation
));
let inspector_id = Id::new_v4();
element(
&mut p,
inspector_id,
Kind::DataType,
Layer::Oa,
"Inspector record",
None,
);
let scenario = p
.add_element(Kind::Scenario, Layer::Oa, "Operational counterpart", None)
.unwrap();
let oa = p
.add_element(
Kind::Occurrence,
Layer::Oa,
"Operational message",
Some(scenario),
)
.unwrap();
*p.elements.get_mut(&oa).unwrap().shared_mut().unwrap() = SharedBody::Occurrence {
body: OccurrenceBody::Exchange {
exchange: None,
sender: None,
receiver: None,
message_kind: MessageKind::Asynchronous,
reply_to: None,
values: vec![],
operation: None,
},
precondition: String::new(),
postcondition: String::new(),
};
p.elements
.get_mut(&scenario)
.unwrap()
.steps_mut()
.unwrap()
.push(oa);
let trace_id = Id::new_v4();
trace(&mut p, trace_id, oa, sa);
let mut app = inspector(p, inspector_id);
let source = app.document.bundle().clone();
let inspector_raw = raw(&app);
let _ = app.update(Message::StartSemanticReview(trace_id));
let _ = app.update(Message::Workflow(workflow::Event::semantic(
semantic_review::Event::Direction(TransitionDirection::SaToOa),
)));
let facet = FacetSelector::Header(HeaderFacet::Parent);
send_review(&mut app, RowEvent::Select(facet.clone()));
send_review(
&mut app,
RowEvent::Decide(facet, Some(SemanticDecision::Candidate)),
);
send_review(&mut app, RowEvent::OperationOmission(true));
assert_eq!(review(&app).direction, TransitionDirection::SaToOa);
assert_eq!(review(&app).operation_omission.as_deref(), Some(""));
let candidate = serde_json::to_value(
review(&app)
.reference_form(app.project())
.unwrap_or_else(|| {
panic!(
"Reverse review candidate: selected={:?}, decisions={:?}, error={:?}",
review(&app).selected,
review(&app).decisions,
review(&app).error
)
})
.recovery_snapshot(),
)
.unwrap();
let mut mount = Mount {
show_review: true,
..Default::default()
};
let fields = mount.frame(&app, &[]).1;
let messages = mount.click(
&app,
fields.field(&WidgetId::new("review-rationale")).0.center(),
);
mount.dispatch(&mut app, messages).await;
let messages = mount.frame(&app, &typing("Engineering reason")).0;
assert_eq!(
messages.len(),
"Engineering reason".chars().count(),
"Every key reaches the legitimate sibling inside Review"
);
assert!(messages.iter().all(|message| matches!(
message,
Message::Workflow(workflow::Event::Semantic(event)) if matches!(event.as_ref(), semantic_review::Event::Row {
event: RowEvent::OperationRationale(_),
..
})
)));
mount.dispatch(&mut app, messages).await;
assert_eq!(
review(&app).operation_omission.as_deref(),
Some("Engineering reason")
);
assert!(
mount
.frame(&app, &[])
.1
.field(&WidgetId::new("review-rationale"))
.1
);
assert_eq!(
serde_json::to_value(
review(&app)
.reference_form(app.project())
.unwrap()
.recovery_snapshot()
)
.unwrap(),
candidate
);
assert_eq!(raw(&app), inspector_raw);
assert_eq!(app.document.bundle(), &source);
assert!(!app.document.can_undo());
}
@@ -0,0 +1,263 @@
use super::*;
const VALID: &str = concat!(
"arcadia 7;\r\n",
"project repair \"Repair 救援\" id \"9e8f0000-0000-4000-8000-000000000001\" {\r\n",
" oa_activity assess \"Assess\" id \"9e8f0000-0000-4000-8000-000000000002\" {}\r\n",
"}\r\n",
);
fn open(app: &mut App, path: &std::path::Path) {
let _ = app.update(Message::Loaded(Ok(Some(
read_snapshot(path.to_owned()).unwrap(),
))));
}
fn edit(app: &mut App, text: &str) {
let _ = app.update(Message::SourceEdit(text_editor::Action::SelectAll));
let _ = app.update(Message::SourceEdit(text_editor::Action::Edit(
text_editor::Edit::Paste(Arc::new(text.into())),
)));
}
fn save(app: &mut App, path: &std::path::Path) {
let receipt = write_snapshot(
app.canvas_document,
app.document.revision(),
app.document.snapshot(),
path.to_owned(),
app.disk_project.clone(),
)
.unwrap();
let _ = app.update(Message::Saved(Ok(Some(receipt))));
}
#[test]
fn repair_to_previous_saved_bytes_requires_save_at_new_destination_and_guards_close() {
for extra in [
" include \"missing.arcadia\";\r\n",
" oa_activity assess \"Duplicate\" id \"9e8f0000-0000-4000-8000-000000000003\" {}\r\n",
] {
let first = tempfile::tempdir().unwrap();
let second = tempfile::tempdir().unwrap();
let first_path = first.path().canonicalize().unwrap().join("project.arcadia");
let second_path = second
.path()
.canonicalize()
.unwrap()
.join("project.arcadia");
let invalid = VALID.strip_suffix("}\r\n").unwrap().to_owned() + extra + "}\r\n";
std::fs::write(&first_path, VALID).unwrap();
std::fs::write(&second_path, &invalid).unwrap();
let mut app = App::with_project(Project::empty());
open(&mut app, &first_path);
assert!(!app.dirty());
let before = app.document.snapshot();
let revision = app.document.revision();
open(&mut app, &second_path);
assert!(!app.source_workspace.applied);
assert!(Arc::ptr_eq(&before, &app.document.snapshot()));
edit(&mut app, VALID);
assert!(app.apply_source(), "{}", app.status);
assert!(app.source_workspace.applied);
assert!(!app.source_dirty());
assert_eq!(app.document.bundle(), &before.bundle);
assert_eq!(app.project(), &before.decoded.project);
assert_eq!(app.document.revision(), revision);
assert!(
!app.document.can_undo(),
"Equal content creates no history step"
);
assert!(app.document.dirty());
assert_eq!(app.hosted_label(), "Unsaved changes");
assert_eq!(std::fs::read(&second_path).unwrap(), invalid.as_bytes());
assert_eq!(std::fs::read(&first_path).unwrap(), VALID.as_bytes());
let close = window::Id::unique();
let task = app.update(Message::Request(Pending::Close(close)));
assert!(iced_runtime::task::into_stream(task).is_none());
assert!(matches!(app.pending, Some(Pending::Close(id)) if id == close));
let _ = app.update(Message::Cancel);
save(&mut app, &second_path);
assert!(!app.dirty());
assert_eq!(app.hosted_label(), "All changes saved");
assert_eq!(std::fs::read(&second_path).unwrap(), VALID.as_bytes());
assert_eq!(std::fs::read(&first_path).unwrap(), VALID.as_bytes());
}
}
#[test]
fn repair_history_compares_to_new_disk_baseline_and_then_successful_save() {
let directory = tempfile::tempdir().unwrap();
let path = directory
.path()
.canonicalize()
.unwrap()
.join("project.arcadia");
std::fs::write(&path, VALID).unwrap();
let mut app = App::with_project(Project::empty());
open(&mut app, &path);
let before = app.document.snapshot();
let invalid = format!("{VALID} unfinished {{");
std::fs::write(&path, &invalid).unwrap();
open(&mut app, &path);
let repaired = format!("{VALID}// Repaired rationale\r\n");
edit(&mut app, &repaired);
assert!(app.apply_source());
assert!(app.dirty());
let _ = app.update(Message::Undo);
assert_eq!(app.document.bundle(), &before.bundle);
assert!(
app.dirty(),
"Old saved bytes are not saved at the repair destination"
);
let _ = app.update(Message::Redo);
assert!(app.dirty());
assert_eq!(std::fs::read(&path).unwrap(), invalid.as_bytes());
std::fs::write(&path, "external edit").unwrap();
let failed = write_snapshot(
app.canvas_document,
app.document.revision(),
app.document.snapshot(),
path.clone(),
app.disk_project.clone(),
)
.unwrap_err();
let _ = app.update(Message::Saved(Err(failed)));
assert!(app.dirty());
assert_eq!(std::fs::read(&path).unwrap(), b"external edit");
std::fs::write(&path, &invalid).unwrap();
save(&mut app, &path);
assert!(!app.dirty());
let _ = app.update(Message::Undo);
assert!(app.dirty());
assert_eq!(app.document.bundle(), &before.bundle);
let _ = app.update(Message::Redo);
assert!(!app.dirty(), "Redo to the new saved snapshot is clean");
assert_eq!(std::fs::read(&path).unwrap(), repaired.as_bytes());
}
#[test]
fn valid_equal_content_at_distinct_destinations_stays_clean() {
let first = tempfile::tempdir().unwrap();
let second = tempfile::tempdir().unwrap();
let first_path = first.path().canonicalize().unwrap().join("project.arcadia");
let second_path = second
.path()
.canonicalize()
.unwrap()
.join("project.arcadia");
std::fs::write(&first_path, VALID).unwrap();
std::fs::write(&second_path, VALID).unwrap();
let mut app = App::with_project(Project::empty());
open(&mut app, &first_path);
let before = app.document.bundle().clone();
open(&mut app, &second_path);
assert_eq!(app.document.bundle(), &before);
assert_eq!(app.path.as_ref(), Some(&second_path));
assert!(!app.dirty());
assert_eq!(app.hosted_label(), "All changes saved");
}
#[test]
fn recovering_applied_repair_rebinds_rolled_back_disk_bytes_without_changing_model() {
let first = tempfile::tempdir().unwrap();
let second = tempfile::tempdir().unwrap();
let first_path = first.path().canonicalize().unwrap().join("project.arcadia");
let root = second.path().canonicalize().unwrap();
let path = root.join("project.arcadia");
let before = VALID.replace("Repair 救援", "Before 救援");
std::fs::write(&first_path, VALID).unwrap();
std::fs::write(&path, VALID).unwrap();
let journal = root.join(".arcadia-save-project.arcadia.journal");
std::fs::create_dir(&journal).unwrap();
std::fs::write(
journal.join("journal.json"),
br#"{"version":1,"entry":"project.arcadia","files":[{"path":"project.arcadia","before":true,"write":true}]}"#,
)
.unwrap();
std::fs::write(journal.join("0.before"), &before).unwrap();
std::fs::write(journal.join("0.after"), VALID).unwrap();
let mut app = App::with_project(Project::empty());
open(&mut app, &first_path);
let accepted = app.document.snapshot();
open(&mut app, &path);
assert_eq!(app.pending_recovery.as_ref(), Some(&path));
assert!(!app.source_workspace.applied);
assert!(app.apply_source());
assert!(
!app.dirty(),
"Current bytes still match disk before rollback"
);
let _ = app.update(Message::SourceRecovered(recover_snapshot(path.clone())));
assert!(app.pending_recovery.is_none());
assert!(!journal.exists());
assert_eq!(std::fs::read(&path).unwrap(), before.as_bytes());
assert!(Arc::ptr_eq(&accepted, &app.document.snapshot()));
assert!(!app.source_dirty());
assert!(app.dirty(), "Recovery changed disk, not the accepted model");
assert_eq!(app.hosted_label(), "Unsaved changes");
save(&mut app, &path);
assert!(!app.dirty());
assert_eq!(std::fs::read(&path).unwrap(), VALID.as_bytes());
}
#[test]
fn recovery_missing_entry_or_failed_reload_preserves_current_destination_save_authority() {
for (current_destination, failed_reload) in
[(true, false), (false, false), (true, true), (false, true)]
{
let directory = tempfile::tempdir().unwrap();
let root = directory.path().canonicalize().unwrap();
let path = root.join("project.arcadia");
std::fs::write(&path, VALID).unwrap();
let mut app = App::with_project(Project::empty());
open(&mut app, &path);
let accepted = app.document.snapshot();
let journal = root.join(".arcadia-save-project.arcadia.journal");
std::fs::create_dir(&journal).unwrap();
std::fs::write(
journal.join("journal.json"),
br#"{"version":1,"entry":"project.arcadia","files":[{"path":"project.arcadia","before":false,"write":true}]}"#,
)
.unwrap();
std::fs::write(journal.join("0.after"), VALID).unwrap();
open(&mut app, &path);
assert!(app.apply_source());
assert!(!app.dirty());
let destination = tempfile::tempdir().unwrap();
let saved_elsewhere = destination
.path()
.canonicalize()
.unwrap()
.join("project.arcadia");
if !current_destination {
save(&mut app, &saved_elsewhere);
assert_eq!(app.path.as_ref(), Some(&saved_elsewhere));
assert_eq!(app.pending_recovery.as_ref(), Some(&path));
}
let mut receipt = recover_snapshot(path.clone()).unwrap();
assert!(
receipt.disk.as_ref().unwrap().bundle.files.is_empty(),
"Missing entry reloads as an empty repair workspace"
);
if failed_reload {
// Separately exercise the adapter's successful-recovery/failed-read
// boundary; first-save rollback itself does not cause a read error.
receipt.disk = Err("Injected post-recovery reload failure".into());
}
let _ = app.update(Message::SourceRecovered(Ok(receipt)));
assert!(!path.exists());
assert!(!journal.exists());
assert!(app.pending_recovery.is_none());
assert!(Arc::ptr_eq(&accepted, &app.document.snapshot()));
assert_eq!(app.dirty(), current_destination);
if current_destination {
let _ = app.update(Message::Request(Pending::New));
assert!(matches!(app.pending, Some(Pending::New)));
} else {
assert_eq!(app.path.as_ref(), Some(&saved_elsewhere));
assert_eq!(std::fs::read(&saved_elsewhere).unwrap(), VALID.as_bytes());
}
}
}
@@ -16,7 +16,7 @@ fn state(app: &App) -> &State {
}
fn row_event(app: &mut App, event: RowEvent) {
let event = state(app).address_event(event);
send(app, Message::Workflow(Event::Semantic(event)));
send(app, Message::Workflow(Event::semantic(event)));
}
fn fixture(reverse: bool, destination_view: bool) -> (App, Id, Id, Id, Option<Id>) {
let mut p = Project::empty();
@@ -114,6 +114,7 @@ impl App {
}
}
match event {
Event::Remove(intent) => self.remove_from_diagram(intent),
Event::Viewport(_, size) => {
if size.width.is_finite()
&& size.height.is_finite()
+316 -224
View File
@@ -14,11 +14,16 @@ use iced::{
};
use oa_core::*;
use std::collections::BTreeMap;
pub(crate) mod canvas_workspace;
#[cfg(test)]
mod contextual_source_tests;
pub(crate) mod creation;
mod form_session;
pub(crate) mod scenario;
// Projection acceptance precedes the coordinated scenario renderer integration.
#[cfg(test)]
mod scenario_lifecycle;
pub(crate) mod source_draft;
pub(crate) mod state_canvas;
pub(crate) mod state_edit;
pub(crate) mod state_menu;
@@ -36,6 +41,7 @@ mod scenario_scroll {
pub(super) fn observe<'a, R: renderer::Renderer + 'a>(
context: scenario::Context,
area: scenario::ScrollArea,
process: bool,
restore: Option<scenario::ScrollRestore>,
content: impl Into<Element<'a, Message, Theme, R>>,
) -> Element<'a, Message, Theme, R> {
@@ -43,28 +49,10 @@ mod scenario_scroll {
content: content.into(),
context,
area,
process: false,
process,
restore,
})
}
pub(super) fn observe_topology<'a>(
context: topology::Context,
area: scenario::ScrollArea,
content: impl Into<Element<'a, Message>>,
) -> Element<'a, Message> {
Element::new(Observer {
content: content.into(),
context: scenario::Context {
document: context.document,
scenario: context.owner,
revision: context.revision,
epoch: context.epoch,
},
area,
process: true,
restore: None,
})
}
struct Observer<'a, R = Renderer> {
content: Element<'a, Message, Theme, R>,
context: scenario::Context,
@@ -335,7 +323,7 @@ mod scenario_scroll {
fn observer(height: f32, context: scenario::Context) -> Observer<'static, ()> {
Observer {
context,
area: scenario::ScrollArea::Canvas,
area: scenario::ScrollArea::Page,
process: false,
restore: None,
content: scrollable(iced::widget::Space::new().width(200.).height(height))
@@ -405,97 +393,96 @@ mod scenario_scroll {
std::time::Instant::now(),
))
};
for area in [ScrollArea::Canvas, ScrollArea::Page] {
for returned_height in [600., 250.] {
let (project, [a, _, _], [use_id, _, _]) = navigation_fixture();
let mut app = App::with_project(project.clone());
let _ = app.update(Message::Select(a));
let _ = app.update(Message::Page(crate::app::Page::Behavior));
let source = app.document.bundle().clone();
let mut view = observer(600., app.scenario_context().unwrap());
view.area = area;
let mut tree = widget::Tree::new(&view as &dyn Widget<Message, Theme, ()>);
for message in update(&mut view, &mut tree, redraw()).0 {
let _ = app.update(message);
}
for message in update(
&mut view,
&mut tree,
iced::Event::Mouse(mouse::Event::WheelScrolled {
delta: mouse::ScrollDelta::Pixels { x: 0., y: -180. },
}),
)
.0
{
let _ = app.update(message);
}
assert_eq!(actual_offset(&mut view, &mut tree), Vector::new(0., 180.));
navigation_event(&mut app, |ctx| CanvasEvent::OpenUse(ctx, use_id));
view = observer(600., app.scenario_context().unwrap());
view.area = area;
view.diff(&mut tree);
for message in update(&mut view, &mut tree, redraw()).0 {
let _ = app.update(message);
}
assert_eq!(actual_offset(&mut view, &mut tree), Vector::ZERO);
navigation_event(&mut app, CanvasEvent::Back);
assert_eq!(app.selected, use_id);
let context = app.scenario_context().unwrap();
let request = app.behavior.scenario.scroll_restore(context, area).unwrap();
view = observer(returned_height, context);
view.area = area;
view.restore = Some(request);
view.diff(&mut tree);
let expected = Vector::new(0., (returned_height - 200.).min(180.));
let messages = update(&mut view, &mut tree, redraw()).0;
assert!(
matches!(messages.as_slice(), [Message::ScenarioCanvas(CanvasEvent::Restored(_, _, _, actual))] if *actual == expected)
);
assert_eq!(actual_offset(&mut view, &mut tree), expected);
for message in messages {
let _ = app.update(message);
}
assert!(
app.behavior
.scenario
.scroll_restore(app.scenario_context().unwrap(), area)
.is_none()
);
// A delayed receipt leaves the same request in the view; later input must not snap back.
for message in update(
&mut view,
&mut tree,
iced::Event::Mouse(mouse::Event::WheelScrolled {
delta: mouse::ScrollDelta::Pixels { x: 0., y: 20. },
}),
)
.0
{
let _ = app.update(message);
}
let moved = expected - Vector::new(0., 20.);
assert_eq!(actual_offset(&mut view, &mut tree), moved);
view.context = app.scenario_context().unwrap();
view.diff(&mut tree);
assert!(update(&mut view, &mut tree, redraw()).0.is_empty());
assert_eq!(actual_offset(&mut view, &mut tree), moved);
navigation_event(&mut app, |ctx| {
CanvasEvent::Restored(ctx, request.token, area, Vector::new(0., 999.))
});
let late_context = app.scenario_context().unwrap();
let _ = app.update(Message::Page(crate::app::Page::Diagram));
let epoch = app.canvas_epoch;
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Restored(
late_context,
request.token,
area,
Vector::ZERO,
)));
assert_eq!(app.canvas_epoch, epoch);
assert_eq!(app.project(), &project);
assert_eq!(app.document.bundle(), &source);
assert!(!app.document.can_undo());
let area = ScrollArea::Page;
for returned_height in [600., 250.] {
let (project, [a, _, _], [use_id, _, _]) = navigation_fixture();
let mut app = App::with_project(project.clone());
let _ = app.update(Message::Select(a));
let _ = app.update(Message::Page(crate::app::Page::Behavior));
let source = app.document.bundle().clone();
let mut view = observer(600., app.scenario_context().unwrap());
view.area = area;
let mut tree = widget::Tree::new(&view as &dyn Widget<Message, Theme, ()>);
for message in update(&mut view, &mut tree, redraw()).0 {
let _ = app.update(message);
}
for message in update(
&mut view,
&mut tree,
iced::Event::Mouse(mouse::Event::WheelScrolled {
delta: mouse::ScrollDelta::Pixels { x: 0., y: -180. },
}),
)
.0
{
let _ = app.update(message);
}
assert_eq!(actual_offset(&mut view, &mut tree), Vector::new(0., 180.));
navigation_event(&mut app, |ctx| CanvasEvent::OpenUse(ctx, use_id));
view = observer(600., app.scenario_context().unwrap());
view.area = area;
view.diff(&mut tree);
for message in update(&mut view, &mut tree, redraw()).0 {
let _ = app.update(message);
}
assert_eq!(actual_offset(&mut view, &mut tree), Vector::ZERO);
navigation_event(&mut app, CanvasEvent::Back);
assert_eq!(app.selected, use_id);
let context = app.scenario_context().unwrap();
let request = app.behavior.scenario.scroll_restore(context, area).unwrap();
view = observer(returned_height, context);
view.area = area;
view.restore = Some(request);
view.diff(&mut tree);
let expected = Vector::new(0., (returned_height - 200.).min(180.));
let messages = update(&mut view, &mut tree, redraw()).0;
assert!(
matches!(messages.as_slice(), [Message::ScenarioCanvas(CanvasEvent::Restored(_, _, _, actual))] if *actual == expected)
);
assert_eq!(actual_offset(&mut view, &mut tree), expected);
for message in messages {
let _ = app.update(message);
}
assert!(
app.behavior
.scenario
.scroll_restore(app.scenario_context().unwrap(), area)
.is_none()
);
// A delayed receipt leaves the same request in the view; later input must not snap back.
for message in update(
&mut view,
&mut tree,
iced::Event::Mouse(mouse::Event::WheelScrolled {
delta: mouse::ScrollDelta::Pixels { x: 0., y: 20. },
}),
)
.0
{
let _ = app.update(message);
}
let moved = expected - Vector::new(0., 20.);
assert_eq!(actual_offset(&mut view, &mut tree), moved);
view.context = app.scenario_context().unwrap();
view.diff(&mut tree);
assert!(update(&mut view, &mut tree, redraw()).0.is_empty());
assert_eq!(actual_offset(&mut view, &mut tree), moved);
navigation_event(&mut app, |ctx| {
CanvasEvent::Restored(ctx, request.token, area, Vector::new(0., 999.))
});
let late_context = app.scenario_context().unwrap();
let _ = app.update(Message::Page(crate::app::Page::Diagram));
let epoch = app.canvas_epoch;
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Restored(
late_context,
request.token,
area,
Vector::ZERO,
)));
assert_eq!(app.canvas_epoch, epoch);
assert_eq!(app.project(), &project);
assert_eq!(app.document.bundle(), &source);
assert!(!app.document.can_undo());
}
}
#[test]
@@ -577,9 +564,9 @@ mod scenario_scroll {
other.scenario = Id::new_v4();
let mut state = topology::State::default();
state.enter(context.document, context.scenario);
state.scrolled(scenario::ScrollArea::Canvas, Vector::new(0., 180.));
state.scrolled(scenario::ScrollArea::Page, Vector::new(0., 180.));
state.enter(other.document, other.scenario);
assert_eq!(state.scroll_offset(), Vector::ZERO);
assert_eq!(state.details_offset(), Vector::ZERO);
view = observer(600., other);
view.process = true;
view.diff(&mut tree);
@@ -596,92 +583,91 @@ mod scenario_scroll {
{
use widget::operation::scrollable::{AbsoluteOffset, scroll_to};
for process in [false, true] {
for area in [scenario::ScrollArea::Canvas, scenario::ScrollArea::Page] {
for change in 0..4 {
let context = scenario::direct_tests::context(Id::new_v4());
let mut view = observer(600., context);
view.process = process;
view.area = area;
let mut tree = widget::Tree::new(&view as &dyn Widget<Message, Theme, ()>);
let node = view.layout(
&mut tree,
&(),
&layout::Limits::new(Size::ZERO, Size::new(800., 700.)),
);
// Model a programmatic reveal before the observer has reported.
// Mounted identity must not depend on its last scroll notification.
let mut operation = scroll_to::<()>(
widget::Id::new("observer-test"),
AbsoluteOffset {
x: None,
y: Some(180.),
},
);
view.operate(&mut tree, Layout::new(&node), &(), &mut operation);
let mut next = context;
match change {
0 => next.scenario = Id::new_v4(),
1 => next.document = Id::new_v4(),
2 => {
next.revision += 1;
next.epoch += 5;
}
_ => {}
let area = scenario::ScrollArea::Page;
for change in 0..4 {
let context = scenario::direct_tests::context(Id::new_v4());
let mut view = observer(600., context);
view.process = process;
view.area = area;
let mut tree = widget::Tree::new(&view as &dyn Widget<Message, Theme, ()>);
let node = view.layout(
&mut tree,
&(),
&layout::Limits::new(Size::ZERO, Size::new(800., 700.)),
);
// Model a programmatic reveal before the observer has reported.
// Mounted identity must not depend on its last scroll notification.
let mut operation = scroll_to::<()>(
widget::Id::new("observer-test"),
AbsoluteOffset {
x: None,
y: Some(180.),
},
);
view.operate(&mut tree, Layout::new(&node), &(), &mut operation);
let mut next = context;
match change {
0 => next.scenario = Id::new_v4(),
1 => next.document = Id::new_v4(),
2 => {
next.revision += 1;
next.epoch += 5;
}
view = observer(if change == 3 { 800. } else { 600. }, next);
view.process = process;
view.area = area;
view.diff(&mut tree);
let redraw = || {
iced::Event::Window(iced::window::Event::RedrawRequested(
std::time::Instant::now(),
))
};
let (messages, _) = update(&mut view, &mut tree, redraw());
let actual = messages
.into_iter()
.filter_map(|message| match message {
Message::TopologyCanvas(topology::CanvasEvent::Scrolled(
c,
reported_area,
offset,
)) => {
assert_eq!(
(c.document, c.owner, c.revision, c.epoch),
(next.document, next.scenario, next.revision, next.epoch)
);
assert_eq!(
std::mem::discriminant(&reported_area),
std::mem::discriminant(&area)
);
Some(offset)
}
Message::ScenarioCanvas(scenario::CanvasEvent::Scrolled(
c,
reported_area,
offset,
)) => {
assert_eq!(c, next);
assert_eq!(
std::mem::discriminant(&reported_area),
std::mem::discriminant(&area)
);
Some(offset)
}
_ => None,
})
.collect::<Vec<_>>();
assert_eq!(
actual,
vec![if change < 2 {
Vector::ZERO
} else {
Vector::new(0., 180.)
}],
"process={process} area={area:?} change={change}"
);
assert!(update(&mut view, &mut tree, redraw()).0.is_empty());
_ => {}
}
view = observer(if change == 3 { 800. } else { 600. }, next);
view.process = process;
view.area = area;
view.diff(&mut tree);
let redraw = || {
iced::Event::Window(iced::window::Event::RedrawRequested(
std::time::Instant::now(),
))
};
let (messages, _) = update(&mut view, &mut tree, redraw());
let actual = messages
.into_iter()
.filter_map(|message| match message {
Message::TopologyCanvas(topology::CanvasEvent::Scrolled(
c,
reported_area,
offset,
)) => {
assert_eq!(
(c.document, c.owner, c.revision, c.epoch),
(next.document, next.scenario, next.revision, next.epoch)
);
assert_eq!(
std::mem::discriminant(&reported_area),
std::mem::discriminant(&area)
);
Some(offset)
}
Message::ScenarioCanvas(scenario::CanvasEvent::Scrolled(
c,
reported_area,
offset,
)) => {
assert_eq!(c, next);
assert_eq!(
std::mem::discriminant(&reported_area),
std::mem::discriminant(&area)
);
Some(offset)
}
_ => None,
})
.collect::<Vec<_>>();
assert_eq!(
actual,
vec![if change < 2 {
Vector::ZERO
} else {
Vector::new(0., 180.)
}],
"process={process} area={area:?} change={change}"
);
assert!(update(&mut view, &mut tree, redraw()).0.is_empty());
}
}
}
@@ -742,6 +728,29 @@ pub enum Event {
Submit,
}
impl Event {
pub fn is_source_back(&self) -> bool {
matches!(
self,
Self::Creation(creation::Event::Source(source_draft::Event::Edit(
_,
source_draft::Edit::Back
))) | Self::Scenario(scenario::Event::Source(source_draft::Event::Edit(
_,
source_draft::Edit::Back
))) | Self::Topology(topology::Event::Source(source_draft::Event::Edit(
_,
source_draft::Edit::Back
)))
)
}
pub fn is_source(&self) -> bool {
matches!(
self,
Self::Creation(creation::Event::Source(_))
| Self::Scenario(scenario::Event::Source(_))
| Self::Topology(topology::Event::Source(_))
)
}
pub fn starts_form(&self) -> bool {
matches!(
self,
@@ -872,6 +881,18 @@ impl State {
/// Clear the active form after its prepared command commits successfully.
/// Rejected commands must leave the form available for repair and retry.
pub fn source_active(&self) -> bool {
self.creation.source_active()
|| self.scenario.source_active()
|| self.topology.source_active()
}
pub fn source_open(&self) -> bool {
self.creation.source_open() || self.scenario.source_open() || self.topology.source_open()
}
pub fn source_back(&mut self) -> bool {
self.creation.source_back() || self.scenario.source_back() || self.topology.source_back()
}
pub fn acknowledge_commit(&mut self) {
self.creation.acknowledge_commit();
self.topology.acknowledge_commit();
@@ -919,6 +940,40 @@ pub fn view(app: &App) -> Element<'_, Message> {
if let Some(card) = app.behavior.creation.view(p) {
return card.map(|event| Message::Behavior(Event::Creation(event)));
}
if let Some(owner) = app.behavior.pending_owner() {
let kind = p.elements.get(&owner).map(oa_core::Element::kind);
let unavailable = p.layer_of(owner) != Some(app.layer)
|| (app.behavior.scenario.pending_owner().is_some() && kind != Some(Kind::Scenario))
|| (app.behavior.topology.pending_owner().is_some()
&& !matches!(kind, Some(Kind::Process | Kind::FunctionalChain)));
if unavailable {
let mut out=column![text("The pending behavior owner is unavailable").size(20),text("Your inputs are retained. Return from the source form or cancel this parent draft before choosing another behavior.").size(13)].spacing(12);
if let Some(name) = app
.behavior
.scenario
.pending_name()
.or_else(|| app.behavior.topology.pending_name())
{
out =
out.push(text(format!("Pending consumer name: {}", short(name, 80))).size(14));
}
if let Some(child) = app.behavior.scenario.source_view(p) {
out = out.push(child.map(|e| Message::Behavior(Event::Scenario(e))));
} else if let Some(child) = app.behavior.topology.source_view(p) {
out = out.push(child.map(|e| Message::Behavior(Event::Topology(e))));
} else {
let cancel = if app.behavior.scenario.pending_owner().is_some() {
Event::Scenario(scenario::Event::Cancel)
} else if app.behavior.topology.pending_owner().is_some() {
Event::Topology(topology::Event::Cancel)
} else {
Event::States(states::Event::Cancel)
};
out = out.push(button("Cancel retained draft").on_press(Message::Behavior(cancel)));
}
return container(out).padding(16).width(Fill).height(Fill).into();
}
}
let choices = p
.elements
.values()
@@ -1084,7 +1139,7 @@ struct Scene {
size: Size,
selected: Id,
scale: f32,
scroll_offset: iced::Vector,
camera_pan: iced::Vector,
interaction: Option<(scenario::Context, scenario::CanvasTool, bool)>,
pending_placement: Option<(scenario::Slot, Option<Id>)>,
lanes: BTreeMap<Id, f32>,
@@ -1138,7 +1193,7 @@ impl Scene {
fn sequence(p: &Project, owner: Id, selected: Id) -> Self {
let mut scene = Self {
scale: 1.,
scroll_offset: iced::Vector::ZERO,
camera_pan: iced::Vector::ZERO,
boxes: vec![],
lines: vec![],
size: Size::new(700., 200.),
@@ -1749,8 +1804,7 @@ impl canvas::Program<Message> for Scene {
if !editable {
state.cancel();
}
// Scrollable marks a pointer outside its visible viewport unavailable,
// even when it lies inside the full (scrolled) canvas rectangle.
// The viewport masks input outside its visible canvas bounds.
let visible = cursor.position().is_some();
if !visible
&& matches!(
@@ -1773,7 +1827,7 @@ impl canvas::Program<Message> for Scene {
SequencePress::Occurrence { .. } => {}
}
}
mouse::Cursor::Available(*position + self.scroll_offset)
mouse::Cursor::Available(*position)
}
canvas::Event::Mouse(mouse::Event::WheelScrolled { .. })
| canvas::Event::Window(iced::window::Event::Resized(_))
@@ -1785,11 +1839,9 @@ impl canvas::Program<Message> for Scene {
}
canvas::Event::Mouse(
mouse::Event::ButtonPressed(_) | mouse::Event::ButtonReleased(_),
) => state
.pointer
.map_or(cursor, |p| mouse::Cursor::Available(p + self.scroll_offset)),
) => state.pointer.map_or(cursor, mouse::Cursor::Available),
canvas::Event::Window(iced::window::Event::RedrawRequested(_)) => {
state.pointer = cursor.position().map(|p| p - self.scroll_offset);
state.pointer = cursor.position();
cursor
}
canvas::Event::Keyboard(iced::keyboard::Event::KeyPressed {
@@ -1801,9 +1853,12 @@ impl canvas::Program<Message> for Scene {
}
_ => cursor,
};
let world = cursor
.position_in(bounds)
.map(|p| Point::new(p.x / self.scale, p.y / self.scale));
let world = cursor.position_in(bounds).map(|p| {
Point::new(
(p.x - self.camera_pan.x) / self.scale,
(p.y - self.camera_pan.y) / self.scale,
)
});
match event {
canvas::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
if !visible {
@@ -1823,14 +1878,14 @@ impl canvas::Program<Message> for Scene {
state.press = Some(SequencePress::Message {
source,
slot,
start: cursor.position()? - self.scroll_offset,
start: cursor.position()?,
first,
moved: false,
});
return Some(Action::request_redraw().and_capture());
}
if let CanvasTool::Add(tool) = tool {
if !tool.canvas_occurrence() {
if !tool.canvas_occurrence() && tool != scenario::Tool::Lifeline {
return None;
}
state.press = Some(SequencePress::Occurrence {
@@ -1840,11 +1895,19 @@ impl canvas::Program<Message> for Scene {
});
return Some(Action::request_redraw().and_capture());
}
if self.reorder_handle().is_some_and(|r| r.contains(point)) {
if let Some(occurrence) = self
.hit(point)
.filter(|id| self.rows.contains_key(id))
.or_else(|| {
self.reorder_handle()
.filter(|r| r.contains(point))
.map(|_| self.selected)
})
{
state.press = Some(SequencePress::Reorder {
occurrence: self.selected,
parent: self.rows[&self.selected].0,
start: cursor.position()? - self.scroll_offset,
occurrence,
parent: self.rows[&occurrence].0,
start: cursor.position()?,
moved: false,
});
return Some(Action::request_redraw().and_capture());
@@ -1918,6 +1981,18 @@ impl canvas::Program<Message> for Scene {
);
}
}
SequencePress::Reorder {
occurrence,
moved: false,
..
} => {
return Some(
Action::publish(Message::ScenarioCanvas(CanvasEvent::Select(
context, occurrence,
)))
.and_capture(),
);
}
SequencePress::Reorder {
occurrence,
parent,
@@ -1955,6 +2030,7 @@ impl canvas::Program<Message> for Scene {
) -> Vec<canvas::Geometry> {
let mut f = Frame::new(renderer, bounds.size());
f.fill_rectangle(Point::ORIGIN, bounds.size(), Color::WHITE);
f.translate(self.camera_pan);
f.scale(self.scale);
for b in &self.boxes {
let outline = matches!(
@@ -2066,9 +2142,12 @@ impl canvas::Program<Message> for Scene {
}
if let Some((context, tool, editable)) = self.interaction {
let active = editable && state.context == Some(context) && state.tool == tool;
let point = cursor
.position_in(bounds)
.map(|p| Point::new(p.x / self.scale, p.y / self.scale));
let point = cursor.position_in(bounds).map(|p| {
Point::new(
(p.x - self.camera_pan.x) / self.scale,
(p.y - self.camera_pan.y) / self.scale,
)
});
let source = active.then_some(state.source).flatten();
let reorder = active.then_some(state.press).flatten().and_then(|press| {
if let SequencePress::Reorder { parent, .. } = press {
@@ -2113,6 +2192,18 @@ impl canvas::Program<Message> for Scene {
} else {
None
};
if reorder.is_some()
&& slot.is_none()
&& let Some(point) = point
{
f.fill_text(Text {
content: "Other operand: move stays in the current sequence".into(),
position: Point::new(point.x + 8., point.y + 12.),
size: (11. / self.scale).into(),
color: Color::from_rgb(0.75, 0.2, 0.15),
..Default::default()
});
}
if let Some(slot) = slot {
f.stroke(
&Path::line(
@@ -2318,25 +2409,26 @@ mod direct_canvas_tests {
}
}
#[test]
fn direct_scrolled_canvas_maps_raw_pointer_events_to_the_visible_slot() {
fn direct_panned_canvas_maps_ordered_pointer_events_to_the_visible_slot() {
let (f, mut scene) = scene();
scene.scroll_offset = iced::Vector::new(35., 140.);
scene.camera_pan = iced::Vector::new(-35., -140.);
let bounds = Rectangle::new(Point::new(20., 230.), scene.size);
let slot = scene.slots.last().unwrap().slot;
let y = scene.slots.last().unwrap().y;
let a = Point::new(bounds.x + scene.lanes[&f.sender], bounds.y + y) - scene.scroll_offset;
let b = Point::new(bounds.x + scene.lanes[&f.receiver], bounds.y + y) - scene.scroll_offset;
let a = Point::new(bounds.x + scene.lanes[&f.sender], bounds.y + y) + scene.camera_pan;
let b = Point::new(bounds.x + scene.lanes[&f.receiver], bounds.y + y) + scene.camera_pan;
assert!(bounds.contains(a) && bounds.contains(b));
let mut state = SequenceInput::default();
// Iced Scrollable passes raw event coordinates and a translated cursor.
// Batched pointer events use their own positions with the final cursor.
for event in [
mouse::Event::CursorMoved { position: a },
mouse::Event::ButtonPressed(mouse::Button::Left),
mouse::Event::CursorMoved { position: b },
] {
send(&scene, &mut state, event, b + scene.scroll_offset, bounds);
send(&scene, &mut state, event, b, bounds);
}
assert!(
matches!(send(&scene, &mut state, mouse::Event::ButtonReleased(mouse::Button::Left), b + scene.scroll_offset, bounds), Some(CanvasEvent::Begin { sender, receiver, slot: actual, .. }) if sender == f.sender && receiver == f.receiver && actual == slot)
matches!(send(&scene, &mut state, mouse::Event::ButtonReleased(mouse::Button::Left), b, bounds), Some(CanvasEvent::Begin { sender, receiver, slot: actual, .. }) if sender == f.sender && receiver == f.receiver && actual == slot)
);
}
#[test]
@@ -0,0 +1,243 @@
//! Shared presentation and input contract; contains no model commands.
use crate::app::{App, Message};
use iced::widget::{button, column, container, opaque, row, scrollable, stack, text, tooltip};
use iced::{Element, Length::Fill, Point, Theme, Vector};
use oa_core::Id;
mod control;
mod focus;
mod viewport;
pub use control::Control;
pub use viewport::Viewport;
pub fn keyboard_enabled(app: &crate::app::App) -> bool {
!app.busy
&& !app.hosted.visible
&& app.workflow.is_none()
&& app.file_prompt.is_none()
&& !app.delete_pending
&& app.pending.is_none()
&& app.model_menu.is_none()
&& !app.reference_input_active()
}
pub fn controls<'a>(
scope: Scope,
keyboard: bool,
scale: f32,
details: Message,
focus: Option<Message>,
zoom: impl Fn(Option<f32>) -> Message,
publish: impl Fn(Event) -> Message + Copy + 'a,
) -> Element<'a, Message> {
let control = |name: &'static str, label: &'static str, message: Option<Message>| {
let message = message.map(|message| intent(scope, name, message));
Control::new(
button(text(label).size(12))
.padding(6)
.on_press_maybe(message.clone()),
scope,
name,
keyboard,
message,
publish,
)
};
row![
control("Details", "Details", Some(details)),
tooltip(control("Canvas", "Canvas", focus),
container(text("F6: enter/leave canvas controls · Tab: next · Enter/Space: activate · Esc: leave").size(12)).padding(7).style(crate::style::card),
tooltip::Position::Bottom),
control("Fit", "Fit", Some(zoom(None))),
control("ZoomOut", "−", Some(zoom(Some(scale / 1.25)))),
text(format!("{:.0}%", scale * 100.)).size(12),
control("ZoomIn", "+", Some(zoom(Some(scale * 1.25)))),
control("ActualSize", "100%", Some(zoom(Some(1.)))),
].spacing(4).align_y(iced::Center).into()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Scope {
pub document: Id,
pub owner: Id,
pub revision: u64,
pub epoch: u64,
}
impl Scope {
pub(crate) fn current(app: &App) -> Option<Self> {
if let Some(context) = app.scenario_context() {
Some(Self {
document: context.document,
owner: context.scenario,
revision: context.revision,
epoch: app.camera_epoch,
})
} else {
let context = app.topology_context()?;
Some(Self {
document: context.document,
owner: context.owner,
revision: context.revision,
epoch: app.camera_epoch,
})
}
}
pub(crate) fn input_owner(app: &App) -> Option<Self> {
if keyboard_enabled(app)
&& !app.behavior.has_pending()
&& !app.draft_dirty()
&& !app.source_dirty()
{
Self::current(app)
} else {
None
}
}
pub fn control_id(self, name: &str) -> iced::advanced::widget::Id {
iced::advanced::widget::Id::from(format!(
"behavior-control:{}:{}:{}:{name}",
self.document, self.owner, self.revision
))
}
}
/// Only a live presentation intent may carry a toolbar action across its own pan.
#[derive(Debug, Clone)]
pub struct Intent {
pub scope: Scope,
pub name: &'static str,
pub message: Box<Message>,
}
pub fn intent(scope: Scope, name: &'static str, message: Message) -> Message {
Message::BehaviorControl(Intent {
scope,
name,
message: Box::new(message),
})
}
#[derive(Debug, Clone, Copy)]
pub enum Event {
CancelGesture,
Pan(Vector),
Zoom { factor: f32, at: Point, base: f32 },
Focus,
Traverse(bool),
ControlFocus(&'static str),
ToggleFocus,
Leave,
}
pub use focus::{focus_task, root as focus_root};
/// Fold relative input in event order. The base scale is used only on leaving Fit.
pub fn change(pan: &mut Vector, zoom: &mut Option<f32>, fit: &mut bool, event: Event) -> bool {
match event {
Event::Pan(delta) if delta.x.is_finite() && delta.y.is_finite() => {
let next = *pan + delta;
if next.x.is_finite() && next.y.is_finite() && next != *pan {
*pan = next;
true
} else {
false
}
}
Event::Zoom { factor, at, base }
if factor.is_finite()
&& factor > 0.
&& base.is_finite()
&& base > 0.
&& at.x.is_finite()
&& at.y.is_finite() =>
{
let old = if *fit { base } else { zoom.unwrap_or(base) };
let next = (old * factor).clamp(0.05, 3.);
if old == next {
return false;
}
let anchor = Vector::new(at.x, at.y);
let next_pan = anchor - (anchor - *pan) * (next / old);
if !next_pan.x.is_finite() || !next_pan.y.is_finite() {
return false;
}
*pan = next_pan;
*zoom = Some(next);
*fit = false;
true
}
_ => false,
}
}
/// Constant-width labelled palette. Arming a tool never reallocates the canvas.
pub fn palette<'a>(
scope: Scope,
keyboard: bool,
publish: impl Fn(Event) -> Message + Copy + 'a,
entries: impl IntoIterator<Item = (&'static str, &'static str, bool, Option<Message>)>,
) -> Element<'a, Message> {
let mut tools = column![].spacing(3);
for (glyph, label, selected, message) in entries {
let message = message.map(|message| intent(scope, glyph, message));
tools = tools.push(tooltip(
container(Control::new(
button(text(glyph).size(12))
.width(34)
.height(30)
.padding(3)
.style(if selected {
button::primary
} else {
crate::style::nav
})
.on_press_maybe(message.clone()),
scope,
glyph,
keyboard,
message,
publish,
))
.id(format!("behavior-tool:{glyph}")),
container(text(label).size(12))
.padding(7)
.style(crate::style::card),
tooltip::Position::Left,
));
}
container(scrollable(tools).id(scope.control_id("palette-scroll")))
.width(40)
.height(Fill)
.padding(3)
.style(crate::style::panel)
.into()
}
/// Header and palette are permanent. Details/editors overlay the same viewport.
pub fn host<'a, Message: 'a, R: iced::advanced::text::Renderer + 'a>(
header: Element<'a, Message, Theme, R>,
palette: Element<'a, Message, Theme, R>,
canvas: Element<'a, Message, Theme, R>,
editor: Option<Element<'a, Message, Theme, R>>,
) -> Element<'a, Message, Theme, R> {
let mut body = stack![canvas];
if let Some(editor) = editor {
body = body.push(
container(opaque(
container(editor)
.width(300)
.height(Fill)
.style(crate::style::card),
))
.align_right(Fill)
.height(Fill)
.padding(8),
);
}
column![header, row![body, palette].height(Fill)]
.spacing(6)
.padding(8)
.width(Fill)
.height(Fill)
.into()
}
#[cfg(test)]
pub(crate) mod tests;
@@ -0,0 +1,256 @@
//! A real focusable keyboard target around the existing styled pointer button.
use super::{Event as CameraEvent, Scope, focus::Member};
use iced::advanced::{
Clipboard, Layout, Shell, Widget, layout, overlay, renderer,
widget::{Operation, Tree, operation, tree},
};
use iced::{Border, Element, Event, Length, Rectangle, Size, Theme, Vector, keyboard, mouse};
#[derive(Default)]
struct Input {
identity: Option<iced::advanced::widget::Id>,
focused: bool,
}
impl operation::Focusable for Input {
fn is_focused(&self) -> bool {
self.focused
}
fn focus(&mut self) {
self.focused = true;
}
fn unfocus(&mut self) {
self.focused = false;
}
}
pub struct Control<'a, Message, R = iced::Renderer>
where
R: renderer::Renderer,
{
content: Element<'a, Message, Theme, R>,
scope: Scope,
name: &'static str,
enabled: bool,
activate: Option<Message>,
publish: Box<dyn Fn(CameraEvent) -> Message + 'a>,
}
impl<'a, Message, R: renderer::Renderer> Control<'a, Message, R> {
pub fn new(
content: impl Into<Element<'a, Message, Theme, R>>,
scope: Scope,
name: &'static str,
enabled: bool,
activate: Option<Message>,
publish: impl Fn(CameraEvent) -> Message + 'a,
) -> Self {
Self {
content: content.into(),
scope,
name,
enabled: enabled && activate.is_some(),
activate,
publish: Box::new(publish),
}
}
}
impl<Message: Clone, R: renderer::Renderer> Widget<Message, Theme, R> for Control<'_, Message, R> {
fn tag(&self) -> tree::Tag {
tree::Tag::of::<Input>()
}
fn state(&self) -> tree::State {
tree::State::new(Input {
identity: Some(self.scope.control_id(self.name)),
focused: false,
})
}
fn children(&self) -> Vec<Tree> {
vec![Tree::new(self.content.as_widget())]
}
fn diff(&self, tree: &mut Tree) {
let state = tree.state.downcast_mut::<Input>();
let identity = self.scope.control_id(self.name);
if state.identity.as_ref() != Some(&identity) || !self.enabled {
*state = Input {
identity: Some(identity),
focused: false,
};
}
tree.diff_children(std::slice::from_ref(&self.content));
}
fn size(&self) -> Size<Length> {
self.content.as_widget().size()
}
fn layout(&mut self, tree: &mut Tree, renderer: &R, limits: &layout::Limits) -> layout::Node {
self.content
.as_widget_mut()
.layout(&mut tree.children[0], renderer, limits)
}
fn operate(
&mut self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &R,
operation: &mut dyn Operation,
) {
let id = self.scope.control_id(self.name);
operation.container(Some(&id), layout.bounds());
if self.enabled {
operation.custom(Some(&id), layout.bounds(), &mut Member(self.scope));
operation.focusable(
Some(&id),
layout.bounds(),
tree.state.downcast_mut::<Input>(),
);
}
operation.traverse(&mut |operation| {
self.content.as_widget_mut().operate(
&mut tree.children[0],
layout,
renderer,
operation,
);
});
}
fn update(
&mut self,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &R,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) {
if shell.is_event_captured() {
return;
}
let state = tree.state.downcast_mut::<Input>();
if self.enabled {
if let Event::Keyboard(keyboard::Event::KeyPressed {
key,
modifiers,
repeat,
..
}) = event
&& state.focused
{
use keyboard::{Key, key::Named};
let action = match key.as_ref() {
Key::Named(Named::Tab) => {
Some((self.publish)(CameraEvent::Traverse(modifiers.shift())))
}
Key::Named(Named::Escape) => Some((self.publish)(CameraEvent::Leave)),
Key::Named(Named::F6) if modifiers.is_empty() && !repeat => {
Some((self.publish)(CameraEvent::ToggleFocus))
}
Key::Named(Named::Enter | Named::Space) if modifiers.is_empty() && !repeat => {
self.activate.clone()
}
_ => None,
};
if let Some(action) = action {
shell.publish(action);
shell.capture_event();
shell.request_redraw();
return;
}
}
if matches!(
event,
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
) && !layout
.bounds()
.intersection(viewport)
.is_some_and(|bounds| cursor.is_over(bounds))
{
state.focused = false;
}
}
if matches!(event, Event::Window(iced::window::Event::Unfocused)) {
state.focused = false;
}
self.content.as_widget_mut().update(
&mut tree.children[0],
event,
layout,
cursor,
renderer,
clipboard,
shell,
viewport,
);
}
fn draw(
&self,
tree: &Tree,
renderer: &mut R,
theme: &Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
) {
self.content.as_widget().draw(
&tree.children[0],
renderer,
theme,
style,
layout,
cursor,
viewport,
);
if self.enabled && tree.state.downcast_ref::<Input>().focused {
renderer.fill_quad(
renderer::Quad {
bounds: layout.bounds(),
border: Border {
color: crate::style::INK,
width: 2.,
..Default::default()
},
..Default::default()
},
iced::Color::TRANSPARENT,
);
}
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &R,
) -> mouse::Interaction {
self.content.as_widget().mouse_interaction(
&tree.children[0],
layout,
cursor,
viewport,
renderer,
)
}
fn overlay<'a>(
&'a mut self,
tree: &'a mut Tree,
layout: Layout<'a>,
renderer: &R,
viewport: &Rectangle,
translation: Vector,
) -> Option<overlay::Element<'a, Message, Theme, R>> {
self.content.as_widget_mut().overlay(
&mut tree.children[0],
layout,
renderer,
viewport,
translation,
)
}
}
impl<'a, Message: Clone + 'a, R: renderer::Renderer + 'a> From<Control<'a, Message, R>>
for Element<'a, Message, Theme, R>
{
fn from(control: Control<'a, Message, R>) -> Self {
Element::new(control)
}
}
@@ -0,0 +1,293 @@
//! Explicit canvas entry; ordinary initial Tab continues to belong to Inspector.
use super::{Event, Scope};
use iced::advanced::widget::{Id, Operation, Tree, operation};
use iced::advanced::{Clipboard, Layout, Shell, Widget, layout, overlay, renderer};
use iced::{Element, Event as InputEvent, Length, Rectangle, Size, Theme, Vector, mouse};
pub(super) struct Member(pub Scope);
pub fn focus_task<Message: Send + 'static>(scope: Scope, event: Event) -> iced::Task<Message> {
iced::advanced::widget::operate(Collect {
scope,
valid: false,
event,
members: vec![],
focused: None,
palette: None,
})
}
struct Collect {
scope: Scope,
valid: bool,
event: Event,
members: Vec<(Id, Rectangle)>,
focused: Option<usize>,
palette: Option<(Rectangle, Rectangle)>,
}
impl<T: Send + 'static> Operation<T> for Collect {
fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn Operation<T>)) {
visit(self);
}
fn custom(&mut self, id: Option<&Id>, bounds: Rectangle, value: &mut dyn std::any::Any) {
if let Some(owner) = value.downcast_ref::<Owner>() {
self.valid = owner.0 == Some(self.scope);
}
if self.valid
&& value
.downcast_ref::<Member>()
.is_some_and(|m| m.0 == self.scope)
&& let Some(id) = id
{
self.members.push((id.clone(), bounds));
}
}
fn focusable(&mut self, id: Option<&Id>, _: Rectangle, value: &mut dyn operation::Focusable) {
if value.is_focused() {
self.focused = self
.members
.iter()
.position(|(member, _)| Some(member) == id)
.or(self.focused);
}
}
fn scrollable(
&mut self,
id: Option<&Id>,
bounds: Rectangle,
content: Rectangle,
_: Vector,
_: &mut dyn operation::Scrollable,
) {
if id == Some(&self.scope.control_id("palette-scroll")) {
self.palette = Some((bounds, content));
}
}
fn finish(&self) -> operation::Outcome<T> {
let canvas = self.scope.control_id("viewport");
// An enabled viewport witnesses the live lifetime. Hidden/dirty/modal
// owners expose no members; a delayed request cannot clear their focus.
if !self.valid || !self.members.iter().any(|(id, _)| *id == canvas) {
return operation::Outcome::None;
}
let target = match self.event {
Event::Focus => Some(canvas),
Event::ControlFocus(name) => Some(self.scope.control_id(name)),
Event::ToggleFocus if self.focused.is_none() => Some(canvas),
Event::ToggleFocus | Event::Leave if self.focused.is_some() => None,
Event::Traverse(back) => {
let Some(current) = self.focused else {
return operation::Outcome::None;
};
let next = if back {
(current + self.members.len() - 1) % self.members.len()
} else {
(current + 1) % self.members.len()
};
Some(self.members[next].0.clone())
}
_ => return operation::Outcome::None,
};
if target
.as_ref()
.is_some_and(|target| !self.members.iter().any(|(id, _)| id == target))
{
return operation::Outcome::None;
}
let reveal = target.as_ref().and_then(|target| {
let (_, field) = self.members.iter().find(|(id, _)| id == target)?;
let (bounds, content) = self.palette?;
// Only the palette has member controls inside this scrollable.
(field.x >= bounds.x && field.x < bounds.x + bounds.width).then(|| {
(field.center_y() - bounds.y - bounds.height / 2.)
.clamp(0., (content.height - bounds.height).max(0.))
})
});
operation::Outcome::Chain(Box::new(Apply {
scope: self.scope,
valid: false,
target,
members: self.members.iter().map(|(id, _)| id.clone()).collect(),
palette: self.scope.control_id("palette-scroll"),
reveal,
}))
}
}
struct Apply {
scope: Scope,
valid: bool,
target: Option<Id>,
members: Vec<Id>,
palette: Id,
reveal: Option<f32>,
}
impl<T> Operation<T> for Apply {
fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn Operation<T>)) {
visit(self);
}
fn custom(&mut self, _: Option<&Id>, _: Rectangle, value: &mut dyn std::any::Any) {
if let Some(owner) = value.downcast_ref::<Owner>() {
self.valid = owner.0 == Some(self.scope);
}
}
fn focusable(&mut self, id: Option<&Id>, _: Rectangle, state: &mut dyn operation::Focusable) {
if !self.valid {
return;
}
if self
.target
.as_ref()
.is_some_and(|target| Some(target) == id)
{
state.focus();
} else if self.target.is_some() || id.is_some_and(|id| self.members.contains(id)) {
state.unfocus();
}
}
fn scrollable(
&mut self,
id: Option<&Id>,
_: Rectangle,
_: Rectangle,
_: Vector,
state: &mut dyn operation::Scrollable,
) {
if self.valid
&& id == Some(&self.palette)
&& let Some(y) = self.reveal
{
state.scroll_to(operation::scrollable::AbsoluteOffset {
x: None,
y: Some(y),
});
}
}
}
// Every Collect/Apply pass observes current ownership before touching any field.
struct Owner(Option<Scope>);
struct Root<'a, Message, R: renderer::Renderer> {
owner: Option<Scope>,
content: Element<'a, Message, Theme, R>,
}
pub fn root<'a, Message: 'a, R: renderer::Renderer + 'a>(
content: impl Into<Element<'a, Message, Theme, R>>,
owner: Option<Scope>,
) -> Element<'a, Message, Theme, R> {
Element::new(Root {
owner,
content: content.into(),
})
}
impl<Message, R: renderer::Renderer> Widget<Message, Theme, R> for Root<'_, Message, R> {
fn children(&self) -> Vec<Tree> {
vec![Tree::new(self.content.as_widget())]
}
fn diff(&self, tree: &mut Tree) {
tree.diff_children(std::slice::from_ref(&self.content));
}
fn size(&self) -> Size<Length> {
self.content.as_widget().size()
}
fn layout(&mut self, tree: &mut Tree, renderer: &R, limits: &layout::Limits) -> layout::Node {
self.content
.as_widget_mut()
.layout(&mut tree.children[0], renderer, limits)
}
fn operate(
&mut self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &R,
operation: &mut dyn Operation,
) {
operation.custom(None, layout.bounds(), &mut Owner(self.owner));
self.content
.as_widget_mut()
.operate(&mut tree.children[0], layout, renderer, operation);
}
fn update(
&mut self,
tree: &mut Tree,
event: &InputEvent,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &R,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) {
self.content.as_widget_mut().update(
&mut tree.children[0],
event,
layout,
cursor,
renderer,
clipboard,
shell,
viewport,
);
}
fn draw(
&self,
tree: &Tree,
renderer: &mut R,
theme: &Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
) {
self.content.as_widget().draw(
&tree.children[0],
renderer,
theme,
style,
layout,
cursor,
viewport,
);
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &R,
) -> mouse::Interaction {
self.content.as_widget().mouse_interaction(
&tree.children[0],
layout,
cursor,
viewport,
renderer,
)
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'b>,
renderer: &R,
viewport: &Rectangle,
translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, R>> {
self.content.as_widget_mut().overlay(
&mut tree.children[0],
layout,
renderer,
viewport,
translation,
)
}
}
@@ -0,0 +1,757 @@
use super::*;
use crate::app::{App, Message};
use iced::advanced::{Layout, Shell, layout, widget};
use iced::futures::{
StreamExt,
stream::{BoxStream, SelectAll},
};
use iced::{Rectangle, Size, mouse};
use iced_runtime::{
Action,
user_interface::{Cache, UserInterface},
};
use std::collections::HashMap;
type Pending = SelectAll<BoxStream<'static, Action<Message>>>;
// iced_winit drains messages while collecting immediately ready actions. A
// pending Task stream survives until its widget operation sends its result.
fn enqueue(task: iced::Task<Message>, ready: &mut Vec<Action<Message>>, pending: &mut Pending) {
if let Some(mut stream) = iced_runtime::task::into_stream(task) {
let waker = iced::futures::task::noop_waker();
let mut context = std::task::Context::from_waker(&waker);
loop {
match stream.poll_next_unpin(&mut context) {
std::task::Poll::Ready(Some(action)) => ready.push(action),
std::task::Poll::Ready(None) => break,
std::task::Poll::Pending => {
pending.push(stream);
break;
}
}
}
}
}
#[derive(Default)]
pub(crate) struct Geometry {
pub containers: HashMap<widget::Id, Rectangle>,
pub focused: Vec<(Option<widget::Id>, Rectangle, bool)>,
pub text_inputs: Vec<Rectangle>,
pub scrolls: HashMap<widget::Id, (Rectangle, Vector)>,
}
impl widget::Operation for Geometry {
fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn widget::Operation)) {
visit(self);
}
fn focusable(
&mut self,
id: Option<&widget::Id>,
bounds: Rectangle,
state: &mut dyn widget::operation::Focusable,
) {
self.focused.push((id.cloned(), bounds, state.is_focused()));
}
fn text_input(
&mut self,
_: Option<&widget::Id>,
bounds: Rectangle,
_: &mut dyn widget::operation::TextInput,
) {
self.text_inputs.push(bounds);
}
fn container(&mut self, id: Option<&widget::Id>, bounds: Rectangle) {
if let Some(id) = id {
self.containers.insert(id.clone(), bounds);
}
}
fn scrollable(
&mut self,
id: Option<&widget::Id>,
bounds: Rectangle,
_: Rectangle,
offset: Vector,
_: &mut dyn widget::operation::Scrollable,
) {
if let Some(id) = id {
self.scrolls.insert(id.clone(), (bounds, offset));
}
}
}
impl Geometry {
pub fn bounds(&self, id: &str) -> Rectangle {
self.containers[&widget::Id::from(id.to_owned())]
}
}
/// Mount the actual App root, production widgets and renderer. Events are
/// delivered in order with Iced's final-batch cursor, then fed through App::update.
/// This deliberately does not seed any private canvas gesture state.
pub(crate) struct Mounted {
pub renderer: iced::Renderer,
cache: Cache,
pub operations: usize,
}
impl Mounted {
pub async fn unshown() -> Self {
use iced::advanced::renderer::Headless;
let renderer = <iced::Renderer as Headless>::new(iced::Font::DEFAULT, 16.into(), None)
.await
.expect("production mounted tests require the supported WGPU headless renderer");
Self {
renderer,
cache: Cache::new(),
operations: 0,
}
}
pub async fn new(app: &mut App) -> Self {
let mut mounted = Self::unshown().await;
mounted.settle(app).await;
mounted
}
pub fn geometry(&mut self, app: &App) -> Geometry {
let mut geometry = Geometry::default();
self.operate(app, &mut geometry);
geometry
}
/// One operation pass, including every nested overlay, via installed Iced.
pub fn operate(&mut self, app: &App, operation: &mut dyn widget::Operation) {
let mut ui = UserInterface::build(
app.view(),
app.window_size,
std::mem::take(&mut self.cache),
&mut self.renderer,
);
ui.operate(&self.renderer, operation);
self.cache = ui.into_cache();
}
pub async fn settle(&mut self, app: &mut App) {
for _ in 0..3 {
self.batch(
app,
&[iced::Event::Window(iced::window::Event::RedrawRequested(
std::time::Instant::now(),
))],
mouse::Cursor::Unavailable,
)
.await;
}
}
pub async fn batch(
&mut self,
app: &mut App,
events: &[iced::Event],
cursor: mouse::Cursor,
) -> Vec<Message> {
let messages = self.input(app, events, cursor);
let mut ready = vec![];
let mut pending = Pending::new();
for message in messages.iter().cloned() {
enqueue(app.update(message), &mut ready, &mut pending);
}
self.drain(app, ready, pending).await;
messages
}
pub fn input(
&mut self,
app: &App,
events: &[iced::Event],
cursor: mouse::Cursor,
) -> Vec<Message> {
let mut messages = vec![];
let mut ui = UserInterface::build(
app.view(),
app.window_size,
std::mem::take(&mut self.cache),
&mut self.renderer,
);
let _ = ui.update(
events,
cursor,
&mut self.renderer,
&mut iced::advanced::clipboard::Null,
&mut messages,
);
self.cache = ui.into_cache();
messages
}
pub async fn perform(&mut self, app: &mut App, task: iced::Task<Message>) {
let mut ready = vec![];
let mut pending = Pending::new();
enqueue(task, &mut ready, &mut pending);
self.drain(app, ready, pending).await;
}
async fn drain(
&mut self,
app: &mut App,
mut ready: Vec<Action<Message>>,
mut pending: Pending,
) {
loop {
let mut outputs = vec![];
if !ready.is_empty() {
// Build only after all App messages in this cycle have run.
let mut ui = UserInterface::build(
app.view(),
app.window_size,
std::mem::take(&mut self.cache),
&mut self.renderer,
);
for action in ready.drain(..) {
match action {
Action::Output(message) => outputs.push(message),
Action::Widget(mut operation) => {
self.operations += 1;
loop {
ui.operate(&self.renderer, &mut *operation);
match operation.finish() {
widget::operation::Outcome::Chain(next) => operation = next,
widget::operation::Outcome::None
| widget::operation::Outcome::Some(()) => break,
}
}
}
action => {
panic!("Unexpected native action in mounted App harness: {action:?}")
}
}
}
self.cache = ui.into_cache();
}
for message in outputs {
enqueue(app.update(message), &mut ready, &mut pending);
}
if !ready.is_empty() {
continue;
}
let Some(action) = pending.next().await else {
break;
};
ready.push(action);
let waker = iced::futures::task::noop_waker();
let mut context = std::task::Context::from_waker(&waker);
while let std::task::Poll::Ready(Some(action)) = pending.poll_next_unpin(&mut context) {
ready.push(action);
}
}
}
pub fn pixels(&mut self, app: &App) -> Vec<u8> {
use iced::advanced::renderer::Headless;
let mut ui = UserInterface::build(
app.view(),
app.window_size,
std::mem::take(&mut self.cache),
&mut self.renderer,
);
// update initializes the nested overlay layout used by draw.
let mut messages = vec![];
let _ = ui.update(
&[],
mouse::Cursor::Unavailable,
&mut self.renderer,
&mut iced::advanced::clipboard::Null,
&mut messages,
);
assert!(
messages.is_empty(),
"An empty draw preparation must not drop App messages"
);
ui.draw(
&mut self.renderer,
&crate::style::theme(),
&iced::advanced::renderer::Style {
text_color: crate::style::INK,
},
mouse::Cursor::Unavailable,
);
self.cache = ui.into_cache();
Headless::screenshot(
&mut self.renderer,
Size::new(app.window_size.width as u32, app.window_size.height as u32),
1.,
crate::style::BG,
)
}
pub async fn click(&mut self, app: &mut App, point: Point) -> Vec<Message> {
self.batch(
app,
&[
iced::Event::Mouse(mouse::Event::CursorMoved { position: point }),
iced::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
iced::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)),
],
mouse::Cursor::Available(point),
)
.await
}
pub async fn pan(&mut self, app: &mut App, start: Point, end: Point) -> Vec<Message> {
self.batch(
app,
&[
iced::Event::Mouse(mouse::Event::CursorMoved { position: start }),
iced::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)),
iced::Event::Mouse(mouse::Event::CursorMoved { position: end }),
iced::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Middle)),
],
mouse::Cursor::Available(end),
)
.await
}
pub fn scroll(&mut self, app: &App, id: &str, offset: f32) {
let mut operation = widget::operation::scrollable::scroll_to::<()>(
widget::Id::from(id.to_owned()),
widget::operation::scrollable::AbsoluteOffset {
x: None,
y: Some(offset),
},
);
self.operate(app, &mut operation);
}
}
pub(crate) fn key(named: iced::keyboard::key::Named) -> iced::Event {
use iced::keyboard::{self, Key};
iced::Event::Keyboard(keyboard::Event::KeyPressed {
key: Key::Named(named),
modified_key: Key::Named(named),
physical_key: keyboard::key::Physical::Unidentified(
keyboard::key::NativeCode::Unidentified,
),
location: keyboard::Location::Standard,
modifiers: keyboard::Modifiers::empty(),
text: None,
repeat: false,
})
}
#[test]
fn relative_camera_zoom_keeps_the_cursor_world_point_and_rejects_nonfinite_inputs() {
for scale in [0.05, 0.5, 1., 2.] {
let mut pan = Vector::new(-130., 75.);
let mut zoom = Some(scale);
let mut fit = false;
let at = Point::new(171., 93.);
let world = (at - Point::ORIGIN - pan) / scale;
change(
&mut pan,
&mut zoom,
&mut fit,
Event::Zoom {
factor: 1.25,
at,
base: scale,
},
);
let after = (at - Point::ORIGIN - pan) / zoom.unwrap();
assert!((after.x - world.x).abs() < 0.001 && (after.y - world.y).abs() < 0.001);
let before = (pan, zoom, fit);
for input in [
Event::Pan(Vector::new(f32::INFINITY, 0.)),
Event::Zoom {
factor: f32::NAN,
at,
base: scale,
},
Event::Zoom {
factor: 0.,
at,
base: scale,
},
] {
assert!(!change(&mut pan, &mut zoom, &mut fit, input));
assert_eq!((pan, zoom, fit), before);
}
}
}
#[test]
fn viewport_ordered_batch_uses_each_event_position_and_cancels_before_camera_motion() {
use iced::widget::Space;
let scope = Scope {
document: Id::new_v4(),
owner: Id::new_v4(),
revision: 2,
epoch: 0,
};
let mut view: Element<'_, Event, Theme, ()> = Viewport::new(
Space::new().width(Fill).height(Fill),
scope,
1.,
true,
|e| e,
)
.into();
let mut tree = widget::Tree::new(view.as_widget());
let node = view
.as_widget_mut()
.layout(
&mut tree,
&(),
&layout::Limits::new(Size::ZERO, Size::new(400., 300.)),
)
.move_to(Point::new(30., 80.));
let start = Point::new(90., 160.);
let middle = Point::new(125., 175.);
let end = Point::new(145., 185.);
let mut messages = vec![];
for event in [
mouse::Event::CursorMoved { position: start },
mouse::Event::ButtonPressed(mouse::Button::Middle),
mouse::Event::CursorMoved { position: middle },
mouse::Event::CursorMoved { position: end },
mouse::Event::ButtonReleased(mouse::Button::Middle),
] {
view.as_widget_mut().update(
&mut tree,
&iced::Event::Mouse(event),
Layout::new(&node),
mouse::Cursor::Available(end),
&(),
&mut iced::advanced::clipboard::Null,
&mut Shell::new(&mut messages),
&Rectangle::with_size(Size::new(700., 600.)),
);
}
assert!(matches!(messages.first(), Some(Event::CancelGesture)));
let total = messages
.iter()
.fold(Vector::ZERO, |total, event| match event {
Event::Pan(delta) => total + *delta,
_ => total,
});
assert_eq!(total, end - start);
}
pub(crate) fn scope(app: &App) -> Scope {
if let Some(context) = app.scenario_context() {
Scope {
document: context.document,
owner: context.scenario,
revision: context.revision,
epoch: app.camera_epoch,
}
} else {
let context = app.topology_context().expect("mounted behavior owner");
Scope {
document: context.document,
owner: context.owner,
revision: context.revision,
epoch: app.camera_epoch,
}
}
}
pub(crate) fn focused(mounted: &mut Mounted, app: &App, name: &str) -> bool {
let id = scope(app).control_id(name);
mounted
.geometry(app)
.focused
.iter()
.any(|(member, _, focused)| member.as_ref() == Some(&id) && *focused)
}
pub(crate) async fn tab_to(mounted: &mut Mounted, app: &mut App, name: &str) {
for _ in 0..32 {
if focused(mounted, app, name) {
return;
}
mounted
.batch(
app,
&[key(iced::keyboard::key::Named::Tab)],
mouse::Cursor::Unavailable,
)
.await;
}
panic!("Keyboard traversal did not reach {name}");
}
pub(crate) fn camera_delta(messages: &[Message]) -> Message {
messages.iter().find(|m| matches!(m,
Message::ScenarioCanvas(crate::behavior::scenario::CanvasEvent::Camera(_, _, Event::Pan(delta)))
| Message::TopologyCanvas(crate::behavior::topology::CanvasEvent::Camera(_, _, Event::Pan(delta))) if *delta != Vector::ZERO
)).expect("actual nonzero camera delta from a mounted gesture").clone()
}
pub(crate) async fn press_middle(mounted: &mut Mounted, app: &mut App, point: Point) {
mounted
.batch(
app,
&[
iced::Event::Mouse(mouse::Event::CursorMoved { position: point }),
iced::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)),
],
mouse::Cursor::Available(point),
)
.await;
}
pub(crate) async fn release_middle(
mounted: &mut Mounted,
app: &mut App,
point: Point,
) -> Vec<Message> {
mounted
.batch(
app,
&[
iced::Event::Mouse(mouse::Event::CursorMoved { position: point }),
iced::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Middle)),
],
mouse::Cursor::Available(point),
)
.await
}
pub(crate) fn assert_drawn_focus(before: &[u8], after: &[u8], size: Size, bounds: Rectangle) {
let width = size.width as usize;
assert_eq!(before.len(), width * size.height as usize * 4);
assert_eq!(after.len(), before.len());
let mut changed = 0;
for y in bounds.y.ceil() as usize..(bounds.y + bounds.height).floor() as usize {
for x in bounds.x.ceil() as usize..(bounds.x + bounds.width).floor() as usize {
if x as f32 - bounds.x > 2.
&& bounds.x + bounds.width - x as f32 > 2.
&& y as f32 - bounds.y > 2.
&& bounds.y + bounds.height - y as f32 > 2.
{
continue;
}
let start = (y * width + x) * 4;
if before[start..start + 3] != after[start..start + 3] {
changed += 1;
}
}
}
assert!(
changed >= bounds.width.min(bounds.height) as usize,
"Focus must change visible border pixels, changed={changed}, bounds={bounds:?}"
);
}
pub(crate) fn character(value: &str) -> iced::Event {
let mut event = key(iced::keyboard::key::Named::Space);
if let iced::Event::Keyboard(iced::keyboard::Event::KeyPressed {
key,
modified_key,
text,
..
}) = &mut event
{
*key = iced::keyboard::Key::Character(value.into());
*modified_key = key.clone();
*text = Some(value.into());
}
event
}
/// Inspect the action without stripping the lifetime from retained packets.
pub(crate) fn intent_action(message: &Message) -> &Message {
match message {
Message::BehaviorControl(intent) => &intent.message,
message => message,
}
}
/// Deliberately retain a returned Chain to test the stronger ownership invariant.
/// Native Iced runs Chain passes synchronously; this is not a native interleaving.
pub(crate) async fn retained_focus_apply(
mounted: &mut Mounted,
app: &mut App,
) -> Box<dyn widget::Operation> {
let entry = mounted.input(
app,
&[key(iced::keyboard::key::Named::F6)],
mouse::Cursor::Unavailable,
);
assert_eq!(
entry.len(),
1,
"actual initial F6 must emit one canvas focus request"
);
let mut stream = iced_runtime::task::into_stream(app.update(entry[0].clone()))
.expect("F6 returns a widget Task");
let Some(Action::Widget(mut operation)) = stream.next().await else {
panic!("F6 first action must be Collect");
};
mounted.operate(app, &mut *operation);
match operation.finish() {
widget::operation::Outcome::Chain(apply) => apply,
_ => panic!("Live Collect must produce its guarded Apply pass"),
}
}
pub(crate) async fn overflow_last_tool(mounted: &mut Mounted, app: &mut App, last: &'static str) {
use iced::keyboard::key::Named;
let geometry = mounted.geometry(app);
let last_bounds = geometry.containers[&scope(app).control_id(last)];
let (rail, offset) = geometry.scrolls[&scope(app).control_id("palette-scroll")];
assert_eq!(offset.y, 0.);
assert!(
last_bounds.y >= rail.y + rail.height,
"last tool starts fully below the overflowing palette: tool={last_bounds:?}, rail={rail:?}"
);
mounted
.batch(app, &[key(Named::F6)], mouse::Cursor::Unavailable)
.await;
tab_to(mounted, app, last).await;
let geometry = mounted.geometry(app);
let mut visible = geometry.containers[&scope(app).control_id(last)];
let (rail, offset) = geometry.scrolls[&scope(app).control_id("palette-scroll")];
assert!(offset.y > 0., "keyboard traversal must scroll the palette");
visible.x -= offset.x;
visible.y -= offset.y;
assert!(
visible.y >= rail.y && visible.y + visible.height <= rail.y + rail.height + 0.5,
"the entire last tool is revealed: tool={visible:?}, rail={rail:?}"
);
let messages = mounted
.batch(app, &[key(Named::Enter)], mouse::Cursor::Unavailable)
.await;
assert!(
messages.iter().any(
|message| matches!(message, Message::BehaviorControl(intent) if intent.name == last)
),
"the revealed last tool is keyboard activated"
);
assert!(focused(mounted, app, last));
let on = mounted.pixels(app);
mounted
.batch(app, &[key(Named::Escape)], mouse::Cursor::Unavailable)
.await;
let off = mounted.pixels(app);
assert_drawn_focus(&off, &on, app.window_size, visible);
}
pub(crate) async fn retained_toolbar_boundaries(
mounted: &mut Mounted,
app: &mut App,
tool: &'static str,
) {
use iced::keyboard::key::Named;
for boundary in ["Chooser", "Dirty", "Picker"] {
let mut packets = vec![];
for name in ["Fit", tool, "Details", "Panels", "Canvas"] {
packets.push(capture_toolbar(mounted, app, name));
}
let lifetime = app.camera_epoch;
let epoch = app.canvas_epoch;
let process = app.behavior.topology.camera_boundary();
let scenario = app.behavior.scenario.camera_boundary();
match boundary {
"Chooser" => {
let draft = app.draft.as_ref().unwrap();
let id = crate::forms::reference_chooser::trigger::field_id(
draft.reference_generation(),
"/parent",
);
let bounds = mounted
.geometry(app)
.focused
.iter()
.find(|(field, _, _)| field.as_ref() == Some(&id))
.expect("real Inspector parent reference trigger")
.1;
let request = draft.reference_open(app.project(), "/parent", bounds);
let task = app.update(Message::FieldReferenceOpen(request));
mounted.perform(app, task).await;
assert!(app.reference_input_active());
mounted
.batch(app, &[key(Named::Escape)], mouse::Cursor::Unavailable)
.await;
assert!(!app.reference_input_active());
}
"Dirty" => {
let task = app.update(Message::Field(
"/name".into(),
"Unsaved camera boundary".into(),
));
mounted.perform(app, task).await;
assert!(app.draft_dirty());
// Every retained packet is also rejected while the owner is dirty.
for packet in packets.iter().cloned() {
let task = app.update(packet);
mounted.perform(app, task).await;
}
assert!(app.draft_dirty());
let task = app.update(Message::RevertProperties);
mounted.perform(app, task).await;
assert!(!app.draft_dirty());
}
"Picker" => {
// Public transitions, with cancellation injected at the external
// picker boundary. Never poll the native dialog future here.
let picker = app.update(Message::Save(true));
if cfg!(target_os = "linux") {
assert!(app.file_prompt.is_some());
mounted.perform(app, picker).await;
let task = app.update(Message::CancelFile);
mounted.perform(app, task).await;
} else {
assert!(app.busy, "Save As enters the real native-picker busy state");
drop(picker);
for packet in packets.iter().cloned() {
let task = app.update(packet);
mounted.perform(app, task).await;
}
let task = app.update(Message::SavePath(None));
mounted.perform(app, task).await;
assert!(!app.busy);
}
}
_ => unreachable!(),
}
assert_ne!(
app.camera_epoch, lifetime,
"{boundary} changes the presentation lifetime"
);
assert_eq!(
app.canvas_epoch, epoch,
"{boundary} cycle leaves semantic context unchanged"
);
let focus_before = mounted
.geometry(app)
.focused
.into_iter()
.filter(|(_, _, value)| *value)
.collect::<Vec<_>>();
let current_lifetime = app.camera_epoch;
for packet in packets {
let task = app.update(packet);
mounted.perform(app, task).await;
}
assert_eq!(
app.camera_epoch, current_lifetime,
"{boundary} retained intents cannot start a new lifetime"
);
assert!(
app.behavior.topology.camera_boundary() == process,
"{boundary} retained toolbar packet changed process presentation"
);
assert!(
app.behavior.scenario.camera_boundary() == scenario,
"{boundary} retained toolbar packet changed scenario presentation"
);
assert_eq!(
mounted
.geometry(app)
.focused
.into_iter()
.filter(|(_, _, value)| *value)
.collect::<Vec<_>>(),
focus_before,
"{boundary} retained toolbar packet stole field focus"
);
}
}
pub(crate) fn capture_toolbar(mounted: &mut Mounted, app: &App, name: &'static str) -> Message {
let point = mounted.geometry(app).containers[&scope(app).control_id(name)].center();
mounted
.input(
app,
&[
iced::Event::Mouse(mouse::Event::CursorMoved { position: point }),
iced::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
iced::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)),
],
mouse::Cursor::Available(point),
)
.into_iter()
.find(|message| matches!(message, Message::BehaviorControl(intent) if intent.name == name))
.expect("actual production toolbar packet")
}
@@ -0,0 +1,426 @@
use super::{Event as CameraEvent, Scope};
use iced::advanced::{
Clipboard, Layout, Shell, Widget, layout, overlay, renderer,
widget::{Operation, Tree, operation, tree},
};
use iced::{Border, Element, Event, Length, Point, Rectangle, Size, Theme, Vector};
use iced::{keyboard, mouse};
#[derive(Default)]
struct Input {
scope: Option<Scope>,
focused: bool,
pointer: Option<Point>,
pan: Option<(mouse::Button, Point)>,
space: bool,
bounds: Option<Rectangle>,
}
impl operation::Focusable for Input {
fn is_focused(&self) -> bool {
self.focused
}
fn focus(&mut self) {
self.focused = true;
}
fn unfocus(&mut self) {
self.focused = false;
self.pan = None;
self.space = false;
}
}
pub struct Viewport<'a, Message, R = iced::Renderer>
where
R: renderer::Renderer,
{
content: Element<'a, Message, Theme, R>,
scope: Scope,
scale: f32,
keyboard: bool,
blank_pan: bool,
publish: Box<dyn Fn(CameraEvent) -> Message + 'a>,
}
impl<'a, Message, R: renderer::Renderer> Viewport<'a, Message, R> {
pub fn new(
content: impl Into<Element<'a, Message, Theme, R>>,
scope: Scope,
scale: f32,
keyboard: bool,
publish: impl Fn(CameraEvent) -> Message + 'a,
) -> Self {
Self {
content: content.into(),
scope,
scale,
keyboard,
blank_pan: false,
publish: Box::new(publish),
}
}
pub fn pan_background(mut self, enabled: bool) -> Self {
self.blank_pan = enabled;
self
}
}
impl<Message, R: renderer::Renderer> Widget<Message, Theme, R> for Viewport<'_, Message, R> {
fn tag(&self) -> tree::Tag {
tree::Tag::of::<Input>()
}
fn state(&self) -> tree::State {
tree::State::new(Input {
scope: Some(self.scope),
..Default::default()
})
}
fn children(&self) -> Vec<Tree> {
vec![Tree::new(self.content.as_widget())]
}
fn diff(&self, tree: &mut Tree) {
let state = tree.state.downcast_mut::<Input>();
if state.scope.is_none_or(|s| {
(s.document, s.owner, s.revision)
!= (self.scope.document, self.scope.owner, self.scope.revision)
}) {
*state = Input::default();
}
if state
.scope
.is_some_and(|scope| scope.epoch != self.scope.epoch)
|| !self.keyboard
{
state.pan = None;
state.space = false;
}
if !self.keyboard {
state.focused = false;
}
state.scope = Some(self.scope);
tree.diff_children(std::slice::from_ref(&self.content));
}
fn size(&self) -> Size<Length> {
Size::new(Length::Fill, Length::Fill)
}
fn layout(&mut self, tree: &mut Tree, renderer: &R, limits: &layout::Limits) -> layout::Node {
self.content
.as_widget_mut()
.layout(&mut tree.children[0], renderer, limits)
}
fn operate(
&mut self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &R,
operation: &mut dyn Operation,
) {
if self.keyboard {
let id = self.scope.control_id("viewport");
operation.custom(
Some(&id),
layout.bounds(),
&mut super::focus::Member(self.scope),
);
operation.focusable(
Some(&id),
layout.bounds(),
tree.state.downcast_mut::<Input>(),
);
}
self.content
.as_widget_mut()
.operate(&mut tree.children[0], layout, renderer, operation);
}
fn update(
&mut self,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &R,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) {
if shell.is_event_captured() {
return;
}
let state = tree.state.downcast_mut::<Input>();
let changed_bounds = state.bounds.is_some_and(|bounds| bounds != layout.bounds());
state.bounds = Some(layout.bounds());
if changed_bounds || !self.keyboard {
state.pan = None;
state.space = false;
}
if !self.keyboard {
state.focused = false;
}
if let Event::Mouse(mouse::Event::CursorMoved { position }) = event {
state.pointer = Some(*position);
}
let pointer = match event {
Event::Mouse(
mouse::Event::ButtonPressed(_)
| mouse::Event::ButtonReleased(_)
| mouse::Event::WheelScrolled { .. },
) => state.pointer.or_else(|| cursor.position()),
Event::Mouse(mouse::Event::CursorMoved { position }) => Some(*position),
_ => cursor.position().or(state.pointer),
};
let visible = layout.bounds().intersection(viewport);
let inside = pointer.is_some_and(|p| visible.is_some_and(|v| v.contains(p)))
&& cursor.position().is_some();
if !self.keyboard {
state.space = false;
}
let mut cancel = changed_bounds;
let mut camera = None;
let mut capture = false;
match event {
Event::Window(iced::window::Event::Unfocused | iced::window::Event::Resized(_)) => {
state.pan = None;
state.space = false;
state.focused = false;
}
Event::Mouse(mouse::Event::CursorLeft) => {
state.pan = None;
state.pointer = None;
}
Event::Mouse(mouse::Event::ButtonPressed(button)) => {
if state.pan.is_some_and(|(held, _)| held != *button) || !inside {
state.pan = None;
state.space = false;
cancel = true;
}
state.focused = inside && self.keyboard;
if inside
&& self.keyboard
&& (*button == mouse::Button::Middle
|| (*button == mouse::Button::Left && state.space))
{
state.pan = pointer.map(|p| (*button, p));
cancel = true;
camera = Some(CameraEvent::CancelGesture);
capture = true;
}
}
Event::Mouse(mouse::Event::CursorMoved { position }) => {
if let Some((button, previous)) = state.pan {
state.pan = Some((button, *position));
camera = Some(CameraEvent::Pan(*position - previous));
capture = true;
}
}
Event::Mouse(mouse::Event::ButtonReleased(button)) => {
if let Some((held, previous)) = state.pan
&& held == *button
{
state.pan = None;
camera = pointer
.filter(|_| inside)
.map(|p| CameraEvent::Pan(p - previous));
capture = true;
}
}
Event::Mouse(mouse::Event::WheelScrolled { delta }) if inside && state.focused => {
state.pan = None;
cancel = true;
let y = match delta {
mouse::ScrollDelta::Lines { y, .. } => *y,
mouse::ScrollDelta::Pixels { y, .. } => *y / 40.,
};
if y != 0. {
let at = pointer.unwrap() - Vector::new(layout.bounds().x, layout.bounds().y);
camera = Some(CameraEvent::Zoom {
factor: 1.1_f32.powf(y.clamp(-8., 8.)),
at,
base: self.scale,
});
capture = true;
}
}
Event::Keyboard(keyboard::Event::KeyPressed {
key: keyboard::Key::Named(keyboard::key::Named::F6),
modifiers,
repeat: false,
..
}) if self.keyboard && modifiers.is_empty() => {
camera = Some(CameraEvent::ToggleFocus);
capture = true;
cancel = true;
}
Event::Keyboard(keyboard::Event::KeyPressed { key, modifiers, .. })
if state.focused && self.keyboard =>
{
use keyboard::{Key, key::Named};
match key.as_ref() {
Key::Named(Named::Space) => {
state.space = true;
cancel = true;
camera = Some(CameraEvent::CancelGesture);
capture = true;
}
Key::Named(Named::Tab) => {
camera = Some(CameraEvent::Traverse(modifiers.shift()));
capture = true;
}
Key::Named(Named::Escape) => {
state.pan = None;
state.space = false;
camera = Some(CameraEvent::Leave);
capture = true;
}
Key::Named(key) if modifiers.is_empty() => {
let delta = match key {
Named::ArrowLeft => Some(Vector::new(60., 0.)),
Named::ArrowRight => Some(Vector::new(-60., 0.)),
Named::ArrowUp => Some(Vector::new(0., 60.)),
Named::ArrowDown => Some(Vector::new(0., -60.)),
_ => None,
};
camera = delta.map(CameraEvent::Pan);
capture = camera.is_some();
cancel = capture;
}
_ => {}
}
}
Event::Keyboard(keyboard::Event::KeyReleased { key, .. })
if *key == keyboard::Key::Named(keyboard::key::Named::Space) =>
{
state.space = false;
}
_ => {}
}
if cancel {
self.content.as_widget_mut().update(
&mut tree.children[0],
&Event::Window(iced::window::Event::Unfocused),
layout,
cursor,
renderer,
clipboard,
shell,
viewport,
);
}
if let Some(event) = camera {
shell.publish((self.publish)(event));
}
if capture {
shell.capture_event();
shell.request_redraw();
return;
}
if matches!(event, Event::Keyboard(_)) && (!state.focused || !self.keyboard) {
return;
}
let child_cursor = if inside {
pointer.map_or(mouse::Cursor::Unavailable, mouse::Cursor::Available)
} else {
mouse::Cursor::Unavailable
};
self.content.as_widget_mut().update(
&mut tree.children[0],
event,
layout,
child_cursor,
renderer,
clipboard,
shell,
viewport,
);
// A semantic hit or armed creation tool gets first refusal. In Select,
// an otherwise unused blank press starts the same camera gesture.
if self.blank_pan
&& self.keyboard
&& inside
&& !shell.is_event_captured()
&& matches!(
event,
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
)
{
state.pan = pointer.map(|p| (mouse::Button::Left, p));
shell.publish((self.publish)(CameraEvent::CancelGesture));
shell.capture_event();
shell.request_redraw();
}
}
fn draw(
&self,
tree: &Tree,
renderer: &mut R,
theme: &Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
) {
renderer.with_layer(layout.bounds(), |renderer| {
self.content.as_widget().draw(
&tree.children[0],
renderer,
theme,
style,
layout,
cursor,
viewport,
);
});
if tree.state.downcast_ref::<Input>().focused && self.keyboard {
renderer.fill_quad(
renderer::Quad {
bounds: layout.bounds(),
border: Border {
color: crate::style::TEAL,
width: 1.,
..Default::default()
},
..Default::default()
},
iced::Color::TRANSPARENT,
);
}
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &R,
) -> mouse::Interaction {
if tree.state.downcast_ref::<Input>().pan.is_some() {
mouse::Interaction::Grabbing
} else {
self.content.as_widget().mouse_interaction(
&tree.children[0],
layout,
cursor,
viewport,
renderer,
)
}
}
fn overlay<'a>(
&'a mut self,
tree: &'a mut Tree,
layout: Layout<'a>,
renderer: &R,
viewport: &Rectangle,
translation: Vector,
) -> Option<overlay::Element<'a, Message, Theme, R>> {
self.content.as_widget_mut().overlay(
&mut tree.children[0],
layout,
renderer,
viewport,
translation,
)
}
}
impl<'a, Message: 'a, R: renderer::Renderer + 'a> From<Viewport<'a, Message, R>>
for Element<'a, Message, Theme, R>
{
fn from(viewport: Viewport<'a, Message, R>) -> Self {
Element::new(viewport)
}
}
File diff suppressed because it is too large Load Diff
+101 -5
View File
@@ -75,6 +75,7 @@ impl Picker {
}
#[derive(Debug, Clone)]
pub enum Event {
Source(super::source_draft::Event),
Open(Layer),
Choose(Family),
Name(String),
@@ -97,6 +98,11 @@ struct Browser {
#[derive(Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct Draft {
#[serde(default)]
source: super::source_draft::State,
#[serde(default = "Id::new_v4")]
invocation: Id,
version: u32,
document: Id,
layer: Layer,
@@ -125,6 +131,17 @@ pub struct Created {
}
impl State {
pub fn source_active(&self) -> bool {
self.draft.as_ref().is_some_and(|d| d.source.engaged())
}
pub fn source_open(&self) -> bool {
self.draft.as_ref().is_some_and(|d| d.source.is_open())
}
pub fn source_back(&mut self) -> bool {
self.draft.as_mut().is_some_and(|d| d.source.back())
}
pub fn has_pending(&self) -> bool {
self.draft.is_some()
}
@@ -174,6 +191,49 @@ impl State {
.command(p)
}
pub fn update(&mut self, p: &Project, event: Event) -> Result<Option<Command>, String> {
if let Event::Source(event) = event {
if self
.draft
.as_mut()
.ok_or("Start the consumer form first.")?
.source
.dismiss(&event)?
{
return Ok(None);
}
let d = self
.draft
.as_ref()
.ok_or("Start the consumer form first.")?;
if d.family != Some(Family::Machine) {
return Err("This reference does not support staged source creation.".into());
}
if d.document != p.id {
return Err("This behavior draft belongs to another document.".into());
}
let mut next = d.clone();
if let Some(id) = next.source.update(
p,
super::source_draft::Scope::Machine { layer: d.layer },
d.invocation,
event,
)? {
next.owner = Some(id);
let preview = next
.source
.preview(p, next.owner)?
.ok_or("New owner preview is missing.")?;
if !eligible(&preview, next.layer, Picker::Owner, id) {
return Err("This participant cannot own the machine.".into());
}
}
self.draft = Some(next);
return Ok(None);
}
if self.source_open() && !matches!(&event, Event::Cancel) {
return Err("Use the new source or go Back before editing the parent form.".into());
}
match event {
Event::Cancel => {
self.draft = None;
@@ -184,6 +244,8 @@ impl State {
return Err("Create or cancel the pending behavior first.".into());
}
self.draft = Some(Draft {
source: Default::default(),
invocation: Id::new_v4(),
version: 1,
document: p.id,
layer,
@@ -332,7 +394,7 @@ fn reference_context(p: &Project, e: &Element) -> String {
};
format!("{} · {} · {location}", e.kind(), e.alias)
}
fn reference_label(p: &Project, id: Id) -> String {
pub(super) fn reference_label(p: &Project, id: Id) -> String {
p.elements
.get(&id)
.map(|e| format!("{} · {}", e.name, reference_context(p, e)))
@@ -340,6 +402,15 @@ fn reference_label(p: &Project, id: Id) -> String {
}
impl Draft {
fn command(&self, p: &Project) -> Result<Command, String> {
self.source.check_scope(
super::source_draft::Scope::Machine { layer: self.layer },
self.owner,
)?;
let preview = self.source.preview(p, self.owner)?;
let command = self.consumer_command(preview.as_ref().unwrap_or(p))?;
self.source.combine(p, self.owner, command)
}
fn consumer_command(&self, p: &Project) -> Result<Command, String> {
if self.document != p.id {
return Err("This draft belongs to another document.".into());
}
@@ -475,6 +546,21 @@ impl State {
p: &'a Project,
) -> Option<UiElement<'a, Event, iced::Theme, R>> {
let d = self.draft.as_ref()?;
if let Some(view) = d.source.view(p) {
return Some(
column![
text(format!(
"State machine name: {}",
crate::views::short(&d.name, 60)
))
.size(12),
view.map(Event::Source)
]
.spacing(6)
.height(Fill)
.into(),
);
}
let mut fields = column![].spacing(10);
let title = if let Some(family) = d.family {
fields = fields.push(
@@ -488,7 +574,7 @@ impl State {
Family::Sequence => {
fields = fields.push(
text(format!(
"Owned by the {} layer root. Involve existing work after creation.",
"Owned by the {} layer root. Add work and exchange contributions after creation.",
d.layer
))
.size(12),
@@ -531,7 +617,17 @@ impl State {
}
}
Family::Machine => {
if let Some(label) = d.source.label(d.owner) {
fields = fields.push(text(label).size(13)).push(text("Create and open commits the new owner and machine together, including the first view only when selected.").size(12));
}
fields = fields.push(picker_view(p, d, Picker::Owner));
fields = fields.push(
button("Create new owner…")
.on_press(Event::Source(super::source_draft::Event::Open(
d.invocation,
)))
.padding(7),
);
fields = fields.push(
checkbox(d.first_view)
.label("Create the first state view")
@@ -544,9 +640,9 @@ impl State {
} else {
for family in [Family::Sequence, Family::Scenario, Family::Machine] {
let purpose = match family {
Family::Sequence => "A path through existing work and exchanges",
Family::Sequence => "A path through work and exchanges",
Family::Scenario => "Ordered interactions from an explicit viewpoint",
Family::Machine => "States and modes of an existing participant",
Family::Machine => "States and modes of an explicitly chosen participant",
};
fields = fields.push(
button(
@@ -616,7 +712,7 @@ fn picker_view<'a, R: iced::advanced::text::Renderer + 'a>(
}
if options.is_empty() {
out = out.push(text(match picker {
Picker::Owner => "No matching participant. Refine the search, or cancel and create a participant in Architecture first.",
Picker::Owner => "No matching participant. Refine the search or choose Create new owner.",
Picker::Context => "No matching context. Refine the search, or leave this optional reference empty.",
}).size(12));
}
@@ -0,0 +1,148 @@
//! Keep behavior field state within one editing session and reset it on replacement.
use iced::advanced::widget::{Operation, Tree, tree};
use iced::advanced::{Clipboard, Layout, Shell, Widget, layout, overlay, renderer};
use iced::{Element, Event, Length, Rectangle, Size, Theme, Vector, mouse};
use oa_core::Id;
struct Identity(Id);
struct Session<'a, Message, R: renderer::Renderer> {
identity: Id,
content: Element<'a, Message, Theme, R>,
}
pub(super) fn reset_on_change<'a, Message: 'a, R: renderer::Renderer + 'a>(
identity: Id,
content: impl Into<Element<'a, Message, Theme, R>>,
) -> Element<'a, Message, Theme, R> {
Element::new(Session {
identity,
content: content.into(),
})
}
impl<Message, R: renderer::Renderer> Widget<Message, Theme, R> for Session<'_, Message, R> {
fn tag(&self) -> tree::Tag {
tree::Tag::of::<Identity>()
}
fn state(&self) -> tree::State {
tree::State::new(Identity(self.identity))
}
fn children(&self) -> Vec<Tree> {
vec![Tree::new(self.content.as_widget())]
}
fn diff(&self, tree: &mut Tree) {
let retained = tree.state.downcast_mut::<Identity>();
if retained.0 != self.identity {
retained.0 = self.identity;
// Iced's keyed column preserves same-length child replacements.
// An editing-session replacement must instead get fresh field state.
tree.children = self.children();
} else {
tree.diff_children(std::slice::from_ref(&self.content));
}
}
fn size(&self) -> Size<Length> {
self.content.as_widget().size()
}
fn layout(&mut self, tree: &mut Tree, renderer: &R, limits: &layout::Limits) -> layout::Node {
self.content
.as_widget_mut()
.layout(&mut tree.children[0], renderer, limits)
}
fn operate(
&mut self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &R,
operation: &mut dyn Operation,
) {
self.content
.as_widget_mut()
.operate(&mut tree.children[0], layout, renderer, operation);
}
fn update(
&mut self,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &R,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) {
self.content.as_widget_mut().update(
&mut tree.children[0],
event,
layout,
cursor,
renderer,
clipboard,
shell,
viewport,
);
}
fn draw(
&self,
tree: &Tree,
renderer: &mut R,
theme: &Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
) {
self.content.as_widget().draw(
&tree.children[0],
renderer,
theme,
style,
layout,
cursor,
viewport,
);
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &R,
) -> mouse::Interaction {
self.content.as_widget().mouse_interaction(
&tree.children[0],
layout,
cursor,
viewport,
renderer,
)
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'b>,
renderer: &R,
viewport: &Rectangle,
translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, R>> {
self.content.as_widget_mut().overlay(
&mut tree.children[0],
layout,
renderer,
viewport,
translation,
)
}
}
+301 -85
View File
@@ -4,7 +4,9 @@ use super::topology::{occurrence, unique_batch};
use super::*;
use iced::widget::{Column, column, text_input};
use std::{cell::RefCell, collections::BTreeSet};
pub(crate) mod focus;
mod workspace;
pub(crate) use workspace::NAME_INPUT;
pub use workspace::{Shelf, view};
#[cfg(test)]
@@ -97,7 +99,9 @@ pub enum CanvasEvent {
Scrolled(Context, ScrollArea, iced::Vector),
Restored(Context, Id, ScrollArea, iced::Vector),
Viewport(Context, Size),
FormShown(Context, Id),
Zoom(Context, Option<f32>),
Camera(Context, u64, super::canvas_workspace::Event),
Focus(Context),
Shelf(Context, Shelf),
OpenUse(Context, Id),
@@ -125,8 +129,7 @@ pub enum CanvasEvent {
}
#[derive(Debug, Clone, Copy)]
pub enum ScrollArea {
Canvas,
// Retained navigation channel, now the independent Selection/outline shelf.
// The independent Details/outline scroller; viewport motion uses Camera.
Page,
}
impl CanvasEvent {
@@ -135,7 +138,9 @@ impl CanvasEvent {
Self::Scrolled(context, _, _)
| Self::Restored(context, ..)
| Self::Viewport(context, _)
| Self::FormShown(context, _)
| Self::Zoom(context, _)
| Self::Camera(context, ..)
| Self::Focus(context)
| Self::Shelf(context, _)
| Self::OpenUse(context, _)
@@ -162,6 +167,7 @@ impl std::fmt::Display for Operator {
}
#[derive(Debug, Clone)]
pub enum Event {
Source(super::source_draft::Event),
Start(Id, Tool),
Cancel,
Submit,
@@ -197,11 +203,14 @@ const VALUE_PAGE_SIZE: usize = 24;
#[derive(Default)]
pub struct State {
draft: Option<Draft>,
form_session: Id,
form_mount: Id,
form_focus_pending: bool,
canvas_tool: Option<(Id, CanvasTool)>,
call_choices: RefCell<Option<(CallChoiceContext, Vec<Choice>)>>,
scroll_offsets: [iced::Vector; 2],
details_offset: iced::Vector,
return_to: Vec<Caller>,
restore: Option<ViewportRestore>,
restore: Option<DetailsRestore>,
presentation: workspace::Presentation,
}
const CALLER_LIMIT: usize = 32;
@@ -211,15 +220,14 @@ pub(crate) struct Caller {
pub occurrence: Id,
target: Id,
tool: CanvasTool,
offsets: [iced::Vector; 2],
details_offset: iced::Vector,
presentation: workspace::Presentation,
}
struct ViewportRestore {
struct DetailsRestore {
document: Id,
owner: Id,
token: Id,
offsets: [iced::Vector; 2],
pending: [bool; 2],
offset: iced::Vector,
}
#[derive(Clone, Copy)]
pub(super) struct ScrollRestore {
@@ -240,6 +248,13 @@ struct CallChoiceContext {
#[derive(Clone, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct Draft {
#[serde(default)]
source: super::source_draft::State,
#[serde(default = "Id::new_v4")]
invocation: Id,
#[serde(default)]
source_origin: Option<(Id, oa_core::Element)>,
owner: Id,
parent: Id,
before: Option<Id>,
@@ -272,6 +287,44 @@ struct Draft {
choice_page: usize,
}
impl State {
pub(crate) fn camera_boundary(&self) -> impl PartialEq + use<> {
(
self.canvas_tool,
self.presentation.focused,
self.presentation.shelf,
self.presentation.viewport,
self.presentation.pan,
self.presentation.zoom,
self.presentation.fit,
)
}
pub(super) fn pending_name(&self) -> Option<&str> {
self.draft.as_ref().map(|d| d.name.as_str())
}
pub(super) fn source_view<'a>(&'a self, p: &'a Project) -> Option<iced::Element<'a, Event>> {
self.draft
.as_ref()?
.source
.view(p)
.map(|v| v.map(Event::Source))
}
pub fn source_active(&self) -> bool {
self.draft.as_ref().is_some_and(|d| d.source.engaged())
}
pub fn source_open(&self) -> bool {
self.draft.as_ref().is_some_and(|d| d.source.is_open())
}
pub fn source_back(&mut self) -> bool {
let returned = self.draft.as_mut().is_some_and(|d| d.source.back());
if returned {
self.remount_form();
}
returned
}
pub(crate) fn enter(&mut self, document: Id, owner: Id) {
if self.presentation.identity != Some((document, owner)) {
self.presentation = workspace::Presentation {
@@ -279,16 +332,38 @@ impl State {
focused: self.presentation.focused,
..Default::default()
};
self.scroll_offsets = [iced::Vector::ZERO; 2];
self.details_offset = iced::Vector::ZERO;
self.restore = None;
}
}
pub(crate) fn viewport(&mut self, size: Size) -> bool {
self.presentation.viewport(size)
}
pub(crate) fn camera(&mut self, event: super::canvas_workspace::Event) -> bool {
let p = &mut self.presentation;
super::canvas_workspace::change(&mut p.pan, &mut p.zoom, &mut p.fit, event)
}
pub(crate) fn zoom(&mut self, scale: Option<f32>) -> bool {
self.presentation.zoom(scale)
}
pub(crate) fn camera_zoom(&mut self, p: &Project, owner: Id, zoom: Option<f32>) -> bool {
let size = Scene::sequence(p, owner, owner).size;
let old = self.presentation.scale(size);
if let Some(next) = zoom {
let viewport = self.presentation.viewport.unwrap_or(Size::new(500., 300.));
self.camera(super::canvas_workspace::Event::Zoom {
factor: next / old,
at: Point::new(viewport.width / 2., viewport.height / 2.),
base: old,
})
} else {
self.zoom(None);
let fitted = self.presentation.scale(size);
self.zoom(Some(fitted));
self.presentation.pan = iced::Vector::ZERO;
true
}
}
pub(crate) fn focused(&self) -> bool {
self.presentation.focused
}
@@ -317,11 +392,11 @@ impl State {
occurrence,
target,
tool: self.canvas_tool(context.scenario),
offsets: self.scroll_offsets,
details_offset: self.details_offset,
presentation: self.presentation.clone(),
});
self.canvas_tool = Some((target, CanvasTool::Select));
self.scroll_offsets = [iced::Vector::ZERO; 2];
self.details_offset = iced::Vector::ZERO;
self.restore = None;
Ok(target)
}
@@ -348,34 +423,30 @@ impl State {
return;
}
self.canvas_tool = Some((frame.owner, frame.tool));
self.scroll_offsets = frame.offsets;
self.details_offset = frame.details_offset;
self.presentation = frame.presentation;
self.presentation.identity = Some((context.document, context.scenario));
self.restore = Some(ViewportRestore {
self.restore = Some(DetailsRestore {
document: context.document,
owner: frame.owner,
token: Id::new_v4(),
offsets: frame.offsets,
pending: [true; 2],
offset: frame.details_offset,
});
}
pub(super) fn scroll_restore(
&self,
context: Context,
area: ScrollArea,
ScrollArea::Page: ScrollArea,
) -> Option<ScrollRestore> {
let index = match area {
ScrollArea::Canvas => 0,
ScrollArea::Page => 1,
};
let restore = self.restore.as_ref().filter(|r| {
r.document == context.document && r.owner == context.scenario && r.pending[index]
})?;
let restore = self
.restore
.as_ref()
.filter(|r| r.document == context.document && r.owner == context.scenario)?;
Some(ScrollRestore {
document: restore.document,
owner: restore.owner,
token: restore.token,
offset: restore.offsets[index],
offset: restore.offset,
})
}
pub(crate) fn restored(
@@ -385,22 +456,12 @@ impl State {
area: ScrollArea,
offset: iced::Vector,
) -> bool {
let index = match area {
ScrollArea::Canvas => 0,
ScrollArea::Page => 1,
};
let Some(restore) = self.restore.as_mut().filter(|r| {
r.document == context.document
&& r.owner == context.scenario
&& r.token == token
&& r.pending[index]
}) else {
if !self.restore.as_ref().is_some_and(|r| {
r.document == context.document && r.owner == context.scenario && r.token == token
}) {
return false;
};
restore.pending[index] = false;
if !restore.pending.contains(&true) {
self.restore = None;
}
self.restore = None;
self.scrolled(area, offset)
}
pub(super) fn recovery_snapshot(&self) -> Result<Option<serde_json::Value>, String> {
@@ -413,28 +474,57 @@ impl State {
pub(super) fn from_recovery(value: serde_json::Value) -> Result<Self, String> {
let draft = serde_json::from_value(value).map_err(|e| e.to_string())?;
let session = Id::new_v4();
Ok(Self {
draft: Some(draft),
form_session: session,
form_mount: session,
form_focus_pending: true,
..Default::default()
})
}
pub(crate) fn scrolled(&mut self, area: ScrollArea, offset: iced::Vector) -> bool {
pub(crate) fn form_session(&self) -> Option<Id> {
self.draft.as_ref().map(|_| self.form_session)
}
pub(crate) fn form_mount(&self) -> Option<Id> {
self.draft
.as_ref()
.filter(|d| !d.source.is_open())
.map(|_| self.form_mount)
}
pub(crate) fn focus_stamp(&self, context: Context) -> Option<focus::Stamp> {
(self.draft.as_ref()?.owner == context.scenario).then_some(focus::Stamp::new(
context,
self.form_session,
self.form_mount()?,
))
}
fn remount_form(&mut self) {
self.form_mount = Id::new_v4();
self.form_focus_pending = true;
}
pub(crate) fn take_form_focus(&mut self, mount: Id) -> bool {
if self.form_mount() == Some(mount) && self.form_focus_pending {
self.form_focus_pending = false;
true
} else {
false
}
}
pub(crate) fn scrolled(&mut self, ScrollArea::Page: ScrollArea, offset: iced::Vector) -> bool {
if !offset.x.is_finite() || !offset.y.is_finite() || offset.x < 0. || offset.y < 0. {
return false;
}
let index = match area {
ScrollArea::Canvas => 0,
ScrollArea::Page => 1,
};
let changed = self.scroll_offsets[index] != offset;
self.scroll_offsets[index] = offset;
let changed = self.details_offset != offset;
self.details_offset = offset;
changed
}
fn scroll_offset(&self) -> iced::Vector {
// The former page scroll now belongs to a sibling outline shelf.
self.scroll_offsets[0]
}
fn call_choices(&self, p: &Project, context: Context, draft: &Draft) -> Vec<Choice> {
let key = CallChoiceContext {
document: context.document,
@@ -452,8 +542,16 @@ impl State {
}
cached.as_ref().unwrap().1.clone()
}
pub(crate) fn created_selection(&self, command: &Command) -> Option<Id> {
pub(crate) fn created_selection(&self, p: &Project, command: &Command) -> Option<Id> {
let draft = self.draft.as_ref()?;
if self
.draft
.as_ref()
.is_some_and(|d| d.source.label(d.target).is_some())
&& self.prepare_pending(p).ok().as_ref() != Some(command)
{
return None;
}
fn created(command: &Command, parent: Id) -> Option<Id> {
match command {
Command::CreateElement(element) if element.parent == Some(parent) => {
@@ -568,6 +666,66 @@ impl State {
}
pub fn update(&mut self, p: &Project, event: Event) -> Result<Option<Command>, String> {
if let Event::Source(event) = event {
if self
.draft
.as_mut()
.ok_or("Start the consumer form first.")?
.source
.dismiss(&event)?
{
self.remount_form();
return Ok(None);
}
let d = self
.draft
.as_ref()
.ok_or("Start the consumer form first.")?;
if d.tool != Tool::Lifeline {
return Err("This reference does not support staged source creation.".into());
}
if d.source_origin.as_ref().is_some_and(|(document, owner)| {
*document != p.id || p.elements.get(&d.owner) != Some(owner)
}) {
return Err("The original consumer changed. Cancel and reopen its form; the current draft is retained.".into());
}
if d.source_origin.is_none() && d.source.engaged() {
return Err("The recovered source draft has no original document binding. Go Back or cancel it.".into());
}
let mut next = d.clone();
if next.source_origin.is_none() && matches!(&event, super::source_draft::Event::Open(_))
{
next.source_origin = Some((
p.id,
p.elements
.get(&d.owner)
.ok_or("The consumer owner no longer exists.")?
.clone(),
));
}
if let Some(id) = next.source.update(
p,
super::source_draft::Scope::Lifeline {
owner: d.owner,
viewpoint: viewpoint(p, d.owner),
},
d.invocation,
event,
)? {
next.target = Some(id);
next.command(p)?;
}
let returned = d.source.is_open() && !next.source.is_open();
self.draft = Some(next);
if returned {
self.remount_form();
}
return Ok(None);
}
if self.source_open() && !matches!(&event, Event::Cancel) {
return Err("Use the new source or go Back before editing the parent form.".into());
}
if self.draft.is_some() && matches!(&event, Event::Start(..)) {
return Err(PENDING_DRAFT.into());
}
@@ -576,7 +734,13 @@ impl State {
if p.elements.get(&owner).map(oa_core::Element::kind) != Some(Kind::Scenario) {
return Err("Choose a scenario.".into());
}
self.form_session = Id::new_v4();
self.form_mount = self.form_session;
self.form_focus_pending = true;
self.draft = Some(Draft {
source: Default::default(),
invocation: Id::new_v4(),
source_origin: Some((p.id, p.elements[&owner].clone())),
owner,
parent: owner,
before: None,
@@ -940,7 +1104,14 @@ fn valid_body_choice(p: &Project, owner: Id, body: OccurrenceBody) -> bool {
execute_batch(p, &[Command::CreateElement(e)]).is_ok()
}
fn sources(p: &Project, d: &Draft) -> Vec<Choice> {
let layer = p.elements[&d.owner].layer();
let Some(owner) = p
.elements
.get(&d.owner)
.filter(|e| e.kind() == Kind::Scenario)
else {
return vec![];
};
let layer = owner.layer();
let mut values: Vec<_> = p
.elements
.values()
@@ -1256,6 +1427,28 @@ impl Draft {
e
}
fn command(&self, p: &Project) -> Result<Command, String> {
if self.source.engaged()
&& self.source_origin.as_ref().is_none_or(|(document, owner)| {
*document != p.id || p.elements.get(&self.owner) != Some(owner)
})
{
return Err("The original consumer document or owner changed. Cancel and reopen its form; your draft is retained.".into());
}
self.source.check_scope(
super::source_draft::Scope::Lifeline {
owner: self.owner,
viewpoint: viewpoint(p, self.owner),
},
self.target,
)?;
let preview = self.source.preview(p, self.target)?;
let command = self.consumer_command(preview.as_ref().unwrap_or(p))?;
self.source.combine(p, self.target, command)
}
fn consumer_command(&self, p: &Project) -> Result<Command, String> {
if p.elements.get(&self.owner).map(oa_core::Element::kind) != Some(Kind::Scenario) {
return Err("The scenario no longer exists. Cancel or restore its owner.".into());
}
if !parents(p, self.owner).iter().any(|c| c.id == self.parent) {
return Err("Choose this scenario or one of its operands as destination.".into());
}
@@ -1266,6 +1459,8 @@ impl Draft {
.unwrap_or(self.tool.label());
let name = if self.name.trim().is_empty() {
default
} else if self.source.label(self.target).is_some() {
self.name.as_str()
} else {
self.name.trim()
};
@@ -1857,20 +2052,31 @@ pub(crate) mod direct_tests {
navigation_event(&mut app, |ctx| {
CanvasEvent::Choose(ctx, CanvasTool::Add(Tool::Reuse))
});
navigation_event(&mut app, |ctx| CanvasEvent::Zoom(ctx, None));
let camera_epoch = app.camera_epoch;
navigation_event(&mut app, |ctx| {
CanvasEvent::Scrolled(ctx, ScrollArea::Canvas, iced::Vector::new(25., 170.))
CanvasEvent::Camera(
ctx,
camera_epoch,
super::super::canvas_workspace::Event::Pan(iced::Vector::new(25., 170.)),
)
});
navigation_event(&mut app, |ctx| {
CanvasEvent::Scrolled(ctx, ScrollArea::Page, iced::Vector::new(0., 80.))
});
navigation_event(&mut app, |ctx| CanvasEvent::OpenUse(ctx, caller));
assert_eq!(app.selected, b);
assert_eq!(app.behavior.scenario.scroll_offset(), iced::Vector::ZERO);
assert_eq!(app.behavior.scenario.presentation.pan, iced::Vector::ZERO);
navigation_event(&mut app, |ctx| {
CanvasEvent::Choose(ctx, CanvasTool::Message(MessageStyle::Timer))
});
let camera_epoch = app.camera_epoch;
navigation_event(&mut app, |ctx| {
CanvasEvent::Scrolled(ctx, ScrollArea::Canvas, iced::Vector::new(0., 115.))
CanvasEvent::Camera(
ctx,
camera_epoch,
super::super::canvas_workspace::Event::Pan(iced::Vector::new(0., 115.)),
)
});
navigation_event(&mut app, |ctx| CanvasEvent::OpenUse(ctx, nested));
assert_eq!(app.selected, c);
@@ -1881,7 +2087,7 @@ pub(crate) mod direct_tests {
CanvasTool::Message(MessageStyle::Timer)
);
assert_eq!(
app.behavior.scenario.scroll_offset(),
app.behavior.scenario.presentation.pan,
iced::Vector::new(0., 115.)
);
navigation_event(&mut app, CanvasEvent::Back);
@@ -1892,11 +2098,11 @@ pub(crate) mod direct_tests {
CanvasTool::Add(Tool::Reuse)
);
assert_eq!(
app.behavior.scenario.scroll_offset(),
app.behavior.scenario.presentation.pan,
iced::Vector::new(25., 170.)
);
assert_eq!(
app.behavior.scenario.scroll_offsets[1],
app.behavior.scenario.details_offset,
iced::Vector::new(0., 80.),
"The caller's independent outline scroll is restored separately"
);
@@ -2074,22 +2280,24 @@ pub(crate) mod direct_tests {
let before = app.document.bundle().clone();
let mut scene = Scene::sequence(app.project(), f.owner, f.call);
scene.interaction = Some((context, CanvasTool::Add(tool), true));
scene.scroll_offset = iced::Vector::new(23., 80.);
let bounds = Rectangle::new(Point::new(20., 40.), scene.size);
scene.camera_pan = iced::Vector::new(23., 80.);
let bounds = Rectangle::new(
Point::new(20., 40.),
Size::new(scene.size.width + 46., scene.size.height + 160.),
);
let y = scene.slots.iter().find(|s| s.slot == slot).unwrap().y;
let x = if on_lifeline {
scene.lanes[&f.sender]
} else {
scene.size.width - 35.
};
let point = Point::new(bounds.x + x, bounds.y + y);
let point = Point::new(bounds.x + x, bounds.y + y) + scene.camera_pan;
assert!(bounds.contains(point));
let mut input = SequenceInput::default();
canvas_mouse(
&scene,
&mut input,
mouse::Event::CursorMoved {
position: point - scene.scroll_offset,
},
mouse::Event::CursorMoved { position: point },
point,
bounds,
);
@@ -2265,10 +2473,10 @@ pub(crate) mod direct_tests {
assert!(!app.behavior.has_pending());
}
let old_context = app.scenario_context().unwrap();
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Scrolled(
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Camera(
old_context,
ScrollArea::Canvas,
iced::Vector::new(0., 100.),
app.camera_epoch,
super::super::canvas_workspace::Event::Pan(iced::Vector::new(0., 100.)),
)));
let _ = app.update(Message::ScenarioCanvas(event(old_context)));
assert!(!app.behavior.has_pending());
@@ -2459,26 +2667,29 @@ pub(crate) mod direct_tests {
);
}
#[test]
fn direct_busy_save_keeps_observed_scroll_current_for_the_next_pointer_gesture() {
fn direct_busy_save_keeps_details_current_and_camera_for_the_next_pointer_gesture() {
let f = fixture();
let mut app = App::with_project(f.project.clone());
let _ = app.update(Message::Select(f.owner));
let _ = app.update(Message::Page(crate::app::Page::Behavior));
let context = app.scenario_context().unwrap();
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Camera(
context,
app.camera_epoch,
super::super::canvas_workspace::Event::Pan(iced::Vector::new(-25., -100.)),
)));
let context = app.scenario_context().unwrap();
let original = app.document.snapshot();
app.busy = true;
for (area, offset) in [
(ScrollArea::Canvas, iced::Vector::new(25., 100.)),
(ScrollArea::Page, iced::Vector::new(0., 40.)),
] {
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Scrolled(
context, area, offset,
)));
}
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Scrolled(
context,
ScrollArea::Page,
iced::Vector::new(0., 40.),
)));
assert!(app.busy);
assert_eq!(
app.behavior.scenario.scroll_offset(),
iced::Vector::new(25., 100.)
app.behavior.scenario.details_offset,
iced::Vector::new(0., 40.)
);
assert_ne!(
app.scenario_context(),
@@ -2489,14 +2700,14 @@ pub(crate) mod direct_tests {
stale.document = Id::new_v4();
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Scrolled(
stale,
ScrollArea::Canvas,
ScrollArea::Page,
iced::Vector::new(0., 900.),
)));
assert_eq!(
app.behavior.scenario.scroll_offset(),
iced::Vector::new(25., 100.)
app.behavior.scenario.details_offset,
iced::Vector::new(0., 40.)
);
let root = std::path::PathBuf::from("scenario-scroll-save-fixture");
let root = std::path::PathBuf::from("scenario-details-save-fixture");
let receipt = crate::app::FileSnapshot {
document: context.document,
revision: context.revision,
@@ -2517,12 +2728,17 @@ pub(crate) mod direct_tests {
let context = app.scenario_context().unwrap();
let mut scene = Scene::sequence(app.project(), f.owner, f.call);
scene.interaction = Some((context, CanvasTool::Message(MessageStyle::CallReply), true));
scene.scroll_offset = app.behavior.scenario.scroll_offset();
assert_eq!(
app.behavior.scenario.presentation.pan,
iced::Vector::new(-25., -100.)
);
scene.camera_pan = app.behavior.scenario.presentation.pan;
let slot = scene.slots.last().unwrap().slot;
let bounds = Rectangle::new(Point::new(20., 250.), scene.size);
let y = bounds.y + scene.slots.last().unwrap().y;
let a = Point::new(bounds.x + scene.lanes[&f.sender], y) - scene.scroll_offset;
let b = Point::new(bounds.x + scene.lanes[&f.receiver], y) - scene.scroll_offset;
let a = Point::new(bounds.x + scene.lanes[&f.sender], y) + scene.camera_pan;
let b = Point::new(bounds.x + scene.lanes[&f.receiver], y) + scene.camera_pan;
assert!(bounds.contains(a) && bounds.contains(b));
let mut input = super::super::SequenceInput::default();
let mut published = vec![];
for event in [
@@ -2536,7 +2752,7 @@ pub(crate) mod direct_tests {
&mut input,
&canvas::Event::Mouse(event),
bounds,
mouse::Cursor::Available(b + scene.scroll_offset),
mouse::Cursor::Available(b),
)
.and_then(|action| action.into_inner().0)
{
@@ -0,0 +1,334 @@
//! A delayed focus request must still own the live parent tree when executed.
//! Collect first: a missing target must not unfocus unrelated fields.
use super::Context;
use iced::advanced::widget::{Id, Operation, Tree, operation};
use iced::advanced::{Clipboard, Layout, Shell, Widget, layout, overlay, renderer};
use iced::{Element, Event, Length, Rectangle, Size, Task, Theme, Vector, mouse};
use oa_core::Id as ModelId;
use std::collections::BTreeMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Stamp {
document: ModelId,
owner: ModelId,
revision: u64,
session: ModelId,
mount: ModelId,
}
impl Stamp {
pub(crate) fn new(context: Context, session: ModelId, mount: ModelId) -> Self {
Self {
document: context.document,
owner: context.scenario,
revision: context.revision,
session,
mount,
}
}
}
pub(crate) fn name_id(stamp: Stamp) -> Id {
Id::from(format!(
"{}:{}:{}:{}:{}:{}",
super::NAME_INPUT,
stamp.document,
stamp.owner,
stamp.revision,
stamp.session,
stamp.mount
))
}
struct Owner(Option<Stamp>);
struct Root<'a, Message, R: renderer::Renderer> {
content: Element<'a, Message, Theme, R>,
owner: Option<Stamp>,
}
pub(crate) fn root<'a, Message: 'a, R: renderer::Renderer + 'a>(
content: impl Into<Element<'a, Message, Theme, R>>,
owner: Option<Stamp>,
) -> Element<'a, Message, Theme, R> {
Element::new(Root {
content: content.into(),
owner,
})
}
impl<Message, R: renderer::Renderer> Widget<Message, Theme, R> for Root<'_, Message, R> {
fn children(&self) -> Vec<Tree> {
vec![Tree::new(self.content.as_widget())]
}
fn diff(&self, tree: &mut Tree) {
tree.diff_children(std::slice::from_ref(&self.content));
}
fn size(&self) -> Size<Length> {
self.content.as_widget().size()
}
fn layout(&mut self, tree: &mut Tree, renderer: &R, limits: &layout::Limits) -> layout::Node {
self.content
.as_widget_mut()
.layout(&mut tree.children[0], renderer, limits)
}
fn operate(
&mut self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &R,
op: &mut dyn Operation,
) {
// This is computed from the current App view, never retained widget state.
op.custom(None, layout.bounds(), &mut Owner(self.owner));
self.content
.as_widget_mut()
.operate(&mut tree.children[0], layout, renderer, op);
}
fn update(
&mut self,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &R,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) {
self.content.as_widget_mut().update(
&mut tree.children[0],
event,
layout,
cursor,
renderer,
clipboard,
shell,
viewport,
);
}
fn draw(
&self,
tree: &Tree,
renderer: &mut R,
theme: &Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
) {
self.content.as_widget().draw(
&tree.children[0],
renderer,
theme,
style,
layout,
cursor,
viewport,
);
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &R,
) -> mouse::Interaction {
self.content.as_widget().mouse_interaction(
&tree.children[0],
layout,
cursor,
viewport,
renderer,
)
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'b>,
renderer: &R,
viewport: &Rectangle,
translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, R>> {
self.content.as_widget_mut().overlay(
&mut tree.children[0],
layout,
renderer,
viewport,
translation,
)
}
}
#[derive(Clone)]
struct Scroll {
index: usize,
bounds: Rectangle,
capacity: Vector,
offset: Vector,
}
struct Collect {
stamp: Stamp,
valid: bool,
translation: Vector,
scrolls: Vec<Scroll>,
pending: Option<Scroll>,
index: usize,
target: Option<(Rectangle, Vec<Scroll>)>,
}
pub(crate) fn task<Message: Send + 'static>(stamp: Stamp) -> Task<Message> {
iced::advanced::widget::operate(Collect {
stamp,
valid: false,
translation: Vector::ZERO,
scrolls: vec![],
pending: None,
index: 0,
target: None,
})
.discard()
}
impl Operation for Collect {
fn custom(&mut self, _: Option<&Id>, _: Rectangle, state: &mut dyn std::any::Any) {
if let Some(owner) = state.downcast_ref::<Owner>() {
self.valid = owner.0 == Some(self.stamp);
}
}
fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn Operation)) {
let previous = (self.translation, self.scrolls.len());
if let Some(scroll) = self.pending.take() {
self.translation += scroll.offset;
self.scrolls.push(scroll);
}
visit(self);
self.translation = previous.0;
self.scrolls.truncate(previous.1);
}
fn scrollable(
&mut self,
_: Option<&Id>,
bounds: Rectangle,
content: Rectangle,
offset: Vector,
_: &mut dyn operation::Scrollable,
) {
self.pending = Some(Scroll {
index: self.index,
bounds: translated(bounds, self.translation),
capacity: Vector::new(
(content.width - bounds.width).max(0.),
(content.height - bounds.height).max(0.),
),
offset,
});
self.index += 1;
}
fn focusable(&mut self, id: Option<&Id>, bounds: Rectangle, _: &mut dyn operation::Focusable) {
if id == Some(&name_id(self.stamp)) {
self.target = Some((translated(bounds, self.translation), self.scrolls.clone()));
}
}
fn finish(&self) -> operation::Outcome<()> {
if !self.valid {
return operation::Outcome::None;
}
let Some((mut bounds, ancestors)) = self.target.clone() else {
return operation::Outcome::None;
};
let mut shifts = BTreeMap::new();
for ancestor in ancestors.iter().rev() {
let dx = reveal(
bounds.x,
bounds.width,
ancestor.bounds.x,
ancestor.bounds.width,
);
let dy = reveal(
bounds.y,
bounds.height,
ancestor.bounds.y,
ancestor.bounds.height,
);
// Drawing offsets are rounded; an already-visible axis must not
// acquire a scroll delta from a fractional capacity alone.
let delta = Vector::new(
if dx == 0. {
0.
} else {
(ancestor.offset.x + dx).clamp(0., ancestor.capacity.x) - ancestor.offset.x
},
if dy == 0. {
0.
} else {
(ancestor.offset.y + dy).clamp(0., ancestor.capacity.y) - ancestor.offset.y
},
);
if delta != Vector::ZERO {
shifts.insert(ancestor.index, delta);
}
bounds = translated(bounds, delta);
}
operation::Outcome::Chain(Box::new(Apply {
stamp: self.stamp,
valid: false,
shifts,
index: 0,
}))
}
}
fn translated(bounds: Rectangle, delta: Vector) -> Rectangle {
Rectangle {
x: bounds.x - delta.x,
y: bounds.y - delta.y,
..bounds
}
}
fn reveal(start: f32, length: f32, viewport: f32, size: f32) -> f32 {
if start < viewport || length > size {
start - viewport
} else {
(start + length - viewport - size).max(0.)
}
}
struct Apply {
stamp: Stamp,
valid: bool,
shifts: BTreeMap<usize, Vector>,
index: usize,
}
impl Operation for Apply {
fn custom(&mut self, _: Option<&Id>, _: Rectangle, state: &mut dyn std::any::Any) {
if let Some(owner) = state.downcast_ref::<Owner>() {
self.valid = owner.0 == Some(self.stamp);
}
}
fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn Operation)) {
visit(self);
}
fn focusable(&mut self, id: Option<&Id>, _: Rectangle, state: &mut dyn operation::Focusable) {
if self.valid {
if id == Some(&name_id(self.stamp)) {
state.focus();
} else {
state.unfocus();
}
}
}
fn scrollable(
&mut self,
_: Option<&Id>,
bounds: Rectangle,
content: Rectangle,
_: Vector,
state: &mut dyn operation::Scrollable,
) {
if self.valid
&& let Some(delta) = self.shifts.get(&self.index)
{
state.scroll_by(
operation::scrollable::AbsoluteOffset {
x: delta.x,
y: delta.y,
},
bounds,
content,
);
}
self.index += 1;
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,488 @@
//! A prerequisite belongs to its pending consumer, never the accepted catalog.
use iced::{
Element as UiElement, Fill,
widget::{button, column, container, row, scrollable, text, text_input},
};
use oa_core::*;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
pub const NAME_INPUT: &str = "behavior-source-name";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Scope {
Lifeline {
owner: Id,
viewpoint: ScenarioViewpoint,
},
Work {
owner: Id,
},
Machine {
layer: Layer,
},
}
impl Scope {
fn owner(self) -> Option<Id> {
match self {
Self::Lifeline { owner, .. } | Self::Work { owner } => Some(owner),
Self::Machine { .. } => None,
}
}
fn layer(self, p: &Project) -> Result<Layer, String> {
match self {
Self::Machine { layer } => Ok(layer),
_ => p
.layer_of(self.owner().unwrap())
.ok_or("The consumer owner no longer exists.".into()),
}
}
fn kinds(self, layer: Layer) -> Vec<Kind> {
match self {
Self::Work { .. } => vec![if layer == Layer::Oa {
Kind::Activity
} else {
Kind::Function
}],
Self::Lifeline {
viewpoint: ScenarioViewpoint::OperationalActivity,
..
} => vec![Kind::Activity],
Self::Lifeline {
viewpoint: ScenarioViewpoint::Functional,
..
} => vec![Kind::Function],
Self::Lifeline {
viewpoint: ScenarioViewpoint::ParticipantInteraction,
..
} => vec![Kind::Entity, Kind::Actor, Kind::Role],
Self::Lifeline {
viewpoint: ScenarioViewpoint::BoundaryInterface,
..
} => vec![Kind::System, Kind::SystemActor],
Self::Machine { .. } if layer == Layer::Oa => vec![Kind::Entity, Kind::Actor],
Self::Machine { .. } => vec![Kind::System, Kind::SystemActor],
_ => vec![],
}
}
}
#[derive(Debug, Clone)]
pub enum Event {
Open(Id),
Edit(Id, Edit),
}
#[derive(Debug, Clone)]
pub enum Edit {
Name(String),
Kind(Kind),
Parent(Option<Id>),
Search(String),
Page(usize),
Back,
Use,
}
#[derive(Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct Draft {
token: Id,
document: Id,
scope: Scope,
layer: Layer,
owner: Option<Element>,
id: Id,
kind: Option<Kind>,
name: String,
parent: Option<Element>,
search: String,
page: usize,
identities: Vec<Id>,
}
#[derive(Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct State {
editing: Option<Draft>,
staged: Option<Draft>,
#[serde(default)]
engaged: bool,
}
impl State {
pub fn check_scope(&self, expected: Scope, selected: Option<Id>) -> Result<(), String> {
if self
.editing
.as_ref()
.or_else(|| self.staged.as_ref().filter(|d| Some(d.id) == selected))
.is_some_and(|d| d.scope != expected)
{
return Err("The recovered source request belongs to a different consumer context. Go Back or cancel this draft.".into());
}
Ok(())
}
pub fn dismiss(&mut self, event: &Event) -> Result<bool, String> {
if let Event::Edit(token, Edit::Back) = event {
if self.editing.as_ref().is_none_or(|d| d.token != *token) {
return Err("This Back action belongs to a previous source form.".into());
}
self.editing = None;
self.engaged = true;
return Ok(true);
}
Ok(false)
}
pub fn engaged(&self) -> bool {
self.engaged || self.editing.is_some() || self.staged.is_some()
}
pub fn is_open(&self) -> bool {
self.editing.is_some()
}
pub fn back(&mut self) -> bool {
self.editing.take().is_some()
}
pub fn label(&self, selected: Option<Id>) -> Option<String> {
self.staged
.as_ref()
.filter(|d| Some(d.id) == selected)
.map(|d| {
format!(
"New · {} · {}",
d.kind.map_or("Choose kind".into(), |k| k.to_string()),
d.name
)
})
}
pub fn update(
&mut self,
p: &Project,
scope: Scope,
invocation: Id,
event: Event,
) -> Result<Option<Id>, String> {
if self.dismiss(&event)? {
return Ok(None);
}
match event {
Event::Open(token) => {
if token != invocation || self.is_open() {
return Err("This source request is stale. Use the current form.".into());
}
let layer = scope.layer(p)?;
let kinds = scope.kinds(layer);
if kinds.is_empty() {
return Err("Create new sources requires a strict scenario viewpoint; use an existing source here.".into());
}
self.engaged = true;
self.editing = Some(Draft {
token: Id::new_v4(),
document: p.id,
scope,
layer,
owner: scope.owner().and_then(|id| p.elements.get(&id).cloned()),
id: Id::new_v4(),
kind: (kinds.len() == 1).then_some(kinds[0]),
name: String::new(),
parent: None,
search: String::new(),
page: 0,
identities: (0..match scope {
Scope::Work { owner } => p.children(owner).count() + 4,
Scope::Lifeline { .. } => 1,
Scope::Machine { .. } => 0,
})
.map(|_| Id::new_v4())
.collect(),
});
Ok(None)
}
Event::Edit(token, edit) => {
let d = self
.editing
.as_mut()
.filter(|d| d.token == token && d.scope == scope && d.document == p.id)
.ok_or("This source response is stale; return to the current form.")?;
match edit {
Edit::Back => {
self.editing = None;
Ok(None)
}
Edit::Use => {
d.command(p)?;
let id = d.id;
self.staged = self.editing.take();
Ok(Some(id))
}
Edit::Name(name) => {
d.name = name;
Ok(None)
}
Edit::Kind(kind) => {
if !scope.kinds(d.layer).contains(&kind) {
return Err("Choose an explicitly supported source kind.".into());
}
d.kind = Some(kind);
d.parent = None;
d.page = 0;
Ok(None)
}
Edit::Parent(parent) => {
let kind = d.kind.ok_or("Choose the source kind first.")?;
if !validation::can_parent(p, kind, d.layer, parent) {
return Err("Choose a valid semantic parent.".into());
}
d.parent = parent.map(|id| p.elements[&id].clone());
Ok(None)
}
Edit::Search(query) => {
d.search = query;
d.page = 0;
Ok(None)
}
Edit::Page(page) => {
d.page = page;
Ok(None)
}
}
}
}
}
pub fn preview(&self, p: &Project, selected: Option<Id>) -> Result<Option<Project>, String> {
if self.is_open() {
return Err("Use this new source or go Back before creating the consumer.".into());
}
self.staged
.as_ref()
.filter(|d| Some(d.id) == selected)
.map(|d| execute_batch(p, &[d.command(p)?]).map_err(|e| e.to_string()))
.transpose()
}
pub fn combine(
&self,
p: &Project,
selected: Option<Id>,
command: Command,
) -> Result<Command, String> {
let Some(d) = self.staged.as_ref().filter(|d| Some(d.id) == selected) else {
return Ok(command);
};
let preview = execute_batch(p, &[d.command(p)?]).map_err(|e| e.to_string())?;
let mut commands = match command {
Command::Batch(commands) => commands,
other => vec![other],
};
let fresh: Vec<_> = commands
.iter()
.filter(|_| !matches!(d.scope, Scope::Machine { .. }))
.filter_map(|c| match c {
Command::CreateElement(e) => Some(e.id),
Command::CreateRelation(r) => Some(r.id),
_ => None,
})
.collect();
if fresh.len() > d.identities.len() {
return Err(
"The path grew beyond this draft. Return and create the source again.".into(),
);
}
let replacements: BTreeMap<_, _> = fresh
.into_iter()
.zip(d.identities.iter().copied())
.collect();
if replacements.values().any(|id| preview.contains(*id)) {
return Err(
"A reserved consumer identity is already used. Start a fresh source draft.".into(),
);
}
let remap = |id: &mut Id| {
if let Some(new) = replacements.get(id) {
*id = *new;
}
};
for c in &mut commands {
match c {
Command::CreateElement(e) => {
remap(&mut e.id);
references::visit_element_refs(e, remap);
e.alias = preview.fresh_alias(&e.name, "contribution", e.id);
}
Command::CreateRelation(r) => {
remap(&mut r.id);
references::visit_relation_refs(r, remap);
r.alias = preview.fresh_alias(&r.name, "path", r.id);
}
Command::EditRelation(_, r) => references::visit_relation_refs(r, remap),
Command::SetSteps(_, steps) => steps.iter_mut().for_each(remap),
// Machine commands already carry their reserved identities.
Command::CreateDiagram(_) | Command::Layout(_) => {}
_ => return Err("Unsupported prerequisite consumer operation.".into()),
}
}
commands.insert(0, d.command(p)?);
let command = super::topology::unique_batch(p, commands);
execute_batch(p, std::slice::from_ref(&command)).map_err(|e| e.to_string())?;
Ok(command)
}
pub fn view<'a, R: iced::advanced::text::Renderer + 'a>(
&'a self,
p: &'a Project,
) -> Option<UiElement<'a, Event, iced::Theme, R>> {
let d = self.editing.as_ref()?;
let event = move |edit| Event::Edit(d.token, edit);
let mut fields = column![
text(format!("New source · {}", d.layer)).size(18),
text(match d.scope {
Scope::Lifeline { owner, viewpoint } =>
format!("For a lifeline in {} · {:?}", p.named(owner), viewpoint),
Scope::Work { owner } => format!("For a contribution in {}", p.named(owner)),
Scope::Machine { .. } => "Owner for the pending state machine".into(),
})
.size(12),
text("The parent form is retained. This source is created only with its consumer.")
.size(12)
]
.spacing(8);
for kind in d.scope.kinds(d.layer) {
fields = fields.push(
button(text(kind.to_string()))
.on_press(event(Edit::Kind(kind)))
.style(if d.kind == Some(kind) {
button::primary
} else {
button::secondary
}),
);
}
fields = fields.push(
text_input("Source name (independent of the consumer name)", &d.name)
.id(NAME_INPUT)
.on_input(move |v| event(Edit::Name(v)))
.padding(8),
);
fields = fields.push(
text(format!(
"Semantic parent: {}",
d.parent
.as_ref()
.map_or(format!("{} layer root", d.layer), |e| format!(
"{} · {} · @{}",
e.name,
e.kind(),
e.alias
))
))
.size(12),
);
if let Some(kind) = d.kind {
fields = fields.push(button("Use layer root").on_press_maybe(
validation::can_parent(p, kind, d.layer, None).then(|| event(Edit::Parent(None))),
));
fields = fields.push(
text_input("Find a semantic parent (optional)", &d.search)
.on_input(move |v| event(Edit::Search(v)))
.padding(6),
);
let query = d.search.to_lowercase();
let mut parents: Vec<_> = p
.elements
.values()
.filter(|e| validation::can_parent(p, kind, d.layer, Some(e.id)))
.map(|e| (e.id, super::creation::reference_label(p, e.id)))
.filter(|(_, label)| label.to_lowercase().contains(&query))
.collect();
parents.sort_by(|a, b| a.1.cmp(&b.1).then(a.0.cmp(&b.0)));
let page = d.page.min(parents.len().saturating_sub(1) / 6);
for (id, label) in parents.iter().skip(page * 6).take(6) {
fields = fields.push(
button(text(label.clone()).size(12))
.on_press(event(Edit::Parent(Some(*id))))
.width(Fill),
);
}
if parents.len() > 6 {
fields = fields.push(
row![
button("Previous").on_press_maybe(
(page > 0).then(|| event(Edit::Page(page.saturating_sub(1))))
),
text(format!("{} / {}", page + 1, parents.len().div_ceil(6))),
button("Next").on_press_maybe(
((page + 1) * 6 < parents.len()).then(|| event(Edit::Page(page + 1)))
)
]
.spacing(6),
);
}
}
let ready = d.command(p);
if let Err(error) = &ready {
fields = fields.push(text(error.clone()).size(12));
}
Some(
container(
column![
scrollable(fields).id("behavior-source-fields").height(Fill),
container(
row![
button("Back · Esc").on_press(event(Edit::Back)),
button("Use this new source")
.on_press_maybe(ready.is_ok().then(|| event(Edit::Use)))
]
.spacing(8)
)
.id("behavior-source-actions")
]
.spacing(10),
)
.padding(12)
.height(Fill)
.width(Fill)
.into(),
)
}
}
impl Draft {
fn command(&self, p: &Project) -> Result<Command, String> {
if self.document != p.id
|| self.scope.layer(p)? != self.layer
|| self
.owner
.as_ref()
.is_some_and(|e| p.elements.get(&e.id) != Some(e))
{
return Err(
"The source request's document or consumer changed. Go Back and reopen Create new."
.into(),
);
}
let kind = self
.kind
.filter(|kind| self.scope.kinds(self.layer).contains(kind))
.ok_or("Choose the source kind explicitly.")?;
if self.name.trim().is_empty() {
return Err("Enter the source name.".into());
}
if p.contains(self.id) {
return Err(
"The reserved source identity is already used. Go Back and start again.".into(),
);
}
let parent = self.parent.as_ref().map(|e| e.id);
if self
.parent
.as_ref()
.is_some_and(|e| p.elements.get(&e.id) != Some(e))
|| !validation::can_parent(p, kind, self.layer, parent)
{
return Err("The semantic parent changed. Choose its intended parent again.".into());
}
let e = Element {
id: self.id,
alias: p.fresh_alias(&self.name, "source", self.id),
name: self.name.clone(),
description: String::new(),
parent,
body: ElementBody::new(kind, self.layer)
.ok_or("This source kind is unavailable in this layer.")?,
};
let command = Command::CreateElement(e);
execute_batch(p, std::slice::from_ref(&command)).map_err(|e| e.to_string())?;
Ok(command)
}
}
+29 -6
View File
@@ -38,7 +38,6 @@ pub struct State {
context: Option<Context>,
tool: Option<Tool>,
pointer: Option<Point>,
focused: bool,
press: Option<Press>,
source: Option<Id>,
preview: Option<(Id, Position)>,
@@ -771,7 +770,6 @@ impl canvas::Program<Event> for StateCanvas<'_> {
}
canvas::Event::Window(iced::window::Event::Unfocused) => {
state.reset();
state.focused = false;
return None;
}
_ => cursor,
@@ -779,14 +777,40 @@ impl canvas::Program<Event> for StateCanvas<'_> {
let window = cursor.position();
let local = window.map(|p| Point::new(p.x - bounds.x, p.y - bounds.y));
let inside = local.is_some_and(|p| Self::content(bounds).contains(p));
if let canvas::Event::Keyboard(keyboard::Event::KeyPressed { key, modifiers, .. }) = event
&& state.focused
{
if let canvas::Event::Keyboard(keyboard::Event::KeyPressed { key, modifiers, .. }) = event {
use keyboard::{Key, key::Named};
if *key == Key::Named(Named::Escape) {
state.reset();
return Some(Action::publish(Event::Choose(Tool::Select)).and_capture());
}
if *key == Key::Named(Named::Delete)
&& self.editable
&& self.tool == Tool::Select
&& state.press.is_none()
&& state.pan_from.is_none()
&& state.source.is_none()
&& (modifiers.is_empty() || *modifiers == keyboard::Modifiers::SHIFT)
{
let scene = self.scene(state)?;
let placement = scene
.view
.nodes
.iter()
.find(|node| node.element == self.selected || node.id == self.selected);
let object = placement.map_or(self.selected, |node| node.element);
if placement.is_none() && !scene.view.edges.iter().any(|edge| edge.id == object) {
return None;
}
return Some(
Action::publish(Event::Remove(crate::app::diagram_removal::Intent {
context: self.context,
object,
placement: placement.map(|node| node.id),
model: modifiers.shift(),
}))
.and_capture(),
);
}
if modifiers.is_empty() {
let pan = match key.as_ref() {
Key::Named(Named::ArrowLeft) => Some(Vector::new(60., 0.)),
@@ -804,7 +828,6 @@ impl canvas::Program<Event> for StateCanvas<'_> {
let scene = self.scene(state)?;
match event {
canvas::Event::Mouse(mouse::Event::ButtonPressed(button)) => {
state.focused = inside;
if !inside {
return None;
}
@@ -24,6 +24,7 @@ pub enum Tool {
#[derive(Debug, Clone)]
pub enum Event {
Remove(crate::app::diagram_removal::Intent),
Choose(Tool),
CreateView(Id),
Select(Context, Id),
@@ -70,6 +71,7 @@ pub enum Event {
impl Event {
pub fn context(&self) -> Option<Context> {
match self {
Self::Remove(intent) => Some(intent.context),
Self::Choose(_) | Self::CreateView(_) => None,
Self::Select(c, _)
| Self::Pan(c, _)
+26 -6
View File
@@ -139,11 +139,14 @@ pub fn popup(app: &App, opened: &Opened, clean: bool) -> Option<Menu<Message>> {
true,
));
}
entries.push(item(
"Hide from this diagram",
Message::Command(Command::Delete(node.id)),
true,
));
entries.push(
item(
"Hide from this diagram",
Message::Command(Command::Delete(node.id)),
true,
)
.shortcut("Delete"),
);
}
}
if let Some(relation) = project.relations.get(&id)
@@ -184,7 +187,24 @@ pub fn popup(app: &App, opened: &Opened, clean: bool) -> Option<Menu<Message>> {
}
if id != project.id {
entries.push(Entry::separator());
entries.push(item("Delete from model…", Message::Delete, true));
entries.push(item("Delete from model…", Message::Delete, true).shortcut(
if context.is_some_and(|context| {
context.diagram != id
&& (request.placement.is_some()
|| projection::project_view(project, context.diagram)
.edges
.iter()
.any(|edge| edge.id == id))
}) {
if request.placement.is_some() {
"Shift+Delete"
} else {
"Delete / Shift+Delete"
}
} else {
""
},
));
}
}
Some(Menu {
+6 -1
View File
@@ -173,7 +173,12 @@ pub fn view<'a>(app: &'a App, machine: Id, mut out: Column<'a, Message>) -> Elem
.width(Fill)
.height(Fill)
.into();
out = out.push(canvas.map(event));
out = out.push(crate::diagram::focus::wrap(
canvas.map(event),
context,
crate::app::diagram_removal::enabled(app),
Message::DiagramTraverse,
));
let base: Element<'_, Message> = container(out.spacing(8))
.padding(14)
.width(Fill)
+143 -20
View File
@@ -38,6 +38,7 @@ impl Tool {
}
#[derive(Debug, Clone)]
pub enum Event {
Source(super::source_draft::Event),
Start(Id, Tool, Option<Id>),
Cancel,
Submit,
@@ -58,7 +59,6 @@ pub enum Event {
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Shelf {
Tools,
Selection,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -79,10 +79,11 @@ pub struct State {
identity: Option<(Id, Id)>,
tool: CanvasTool,
expanded: BTreeSet<Id>,
scroll_offsets: [iced::Vector; 2],
return_to: Vec<(Id, Id, Option<oa_core::topology_context::ContextAddress>)>,
details_offset: iced::Vector,
return_to: Vec<direct::ReturnFrame>,
draft: Option<Draft>,
viewport: Option<Size>,
pan: iced::Vector,
zoom: Option<f32>,
fit: bool,
pub(crate) focused: bool,
@@ -90,15 +91,22 @@ pub struct State {
selected_context: Option<oa_core::topology_context::ContextAddress>,
shelf: Option<Shelf>,
}
#[derive(Default, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct Browser {
search: String,
page: usize,
}
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct Draft {
#[serde(default)]
source: super::source_draft::State,
#[serde(default = "Id::new_v4")]
invocation: Id,
#[serde(default)]
source_origin: Option<(Id, oa_core::Element)>,
#[serde(with = "recovery_insertion")]
insertion: Insertion,
fixed: bool,
@@ -117,6 +125,40 @@ struct Draft {
contextual: Option<contextual::Draft>,
}
impl State {
pub(crate) fn camera_boundary(&self) -> impl PartialEq + use<> {
(
self.tool,
self.focused,
self.shelf,
self.viewport,
self.pan,
self.zoom,
self.fit,
)
}
pub(super) fn pending_name(&self) -> Option<&str> {
self.draft.as_ref().map(|d| d.name.as_str())
}
pub(super) fn source_view<'a>(&'a self, p: &'a Project) -> Option<iced::Element<'a, Event>> {
self.draft
.as_ref()?
.source
.view(p)
.map(|v| v.map(Event::Source))
}
pub fn source_active(&self) -> bool {
self.draft.as_ref().is_some_and(|d| d.source.engaged())
}
pub fn source_open(&self) -> bool {
self.draft.as_ref().is_some_and(|d| d.source.is_open())
}
pub fn source_back(&mut self) -> bool {
self.draft.as_mut().is_some_and(|d| d.source.back())
}
pub(super) fn recovery_snapshot(&self) -> Result<Option<serde_json::Value>, String> {
self.draft
.as_ref()
@@ -152,6 +194,58 @@ impl State {
}
pub fn update(&mut self, p: &Project, event: Event) -> Result<Option<Command>, String> {
if let Event::Source(event) = event {
if self
.draft
.as_mut()
.ok_or("Start the consumer form first.")?
.source
.dismiss(&event)?
{
return Ok(None);
}
let d = self
.draft
.as_ref()
.ok_or("Start the consumer form first.")?;
if d.tool != Tool::Work {
return Err("This reference does not support staged source creation.".into());
}
if d.source_origin.as_ref().is_some_and(|(document, owner)| {
*document != p.id || p.elements.get(&d.owner) != Some(owner)
}) {
return Err("The original consumer changed. Cancel and reopen its form; the current draft is retained.".into());
}
if d.source_origin.is_none() && d.source.engaged() {
return Err("The recovered source draft has no original document binding. Go Back or cancel it.".into());
}
let mut next = d.clone();
if next.source_origin.is_none() && matches!(&event, super::source_draft::Event::Open(_))
{
next.source_origin = Some((
p.id,
p.elements
.get(&d.owner)
.ok_or("The consumer owner no longer exists.")?
.clone(),
));
}
if let Some(id) = next.source.update(
p,
super::source_draft::Scope::Work { owner: d.owner },
d.invocation,
event,
)? {
next.target = Some(id);
next.command(p)?;
}
self.draft = Some(next);
return Ok(None);
}
if self.source_open() && !matches!(&event, Event::Cancel) {
return Err("Use the new source or go Back before editing the parent form.".into());
}
if self.draft.is_some() && matches!(&event, Event::Start(..)) {
return Err(PENDING_DRAFT.into());
}
@@ -184,6 +278,9 @@ impl State {
}
});
self.draft = Some(Draft {
source: Default::default(),
invocation: Id::new_v4(),
source_origin: Some((p.id, p.elements[&owner].clone())),
insertion: Insertion::default(),
fixed: false,
browsers: Default::default(),
@@ -253,8 +350,8 @@ impl State {
}
fn fit_area(&self, fallback_width: f32) -> Size {
// The sensor measures the inner scroller, independent of graph content.
// Leave a small gutter for scrollbars; before first layout use a compact
// The sensor measures the viewport, independent of graph content.
// Leave a small content gutter; before first layout use a compact
// provisional viewport and recompute when the sensor reports actual size.
let viewport = self.viewport.unwrap_or(Size::new(fallback_width, 180.));
Size::new(
@@ -263,10 +360,7 @@ impl State {
)
}
fn canvas_offset(&self) -> iced::Vector {
self.scroll_offsets[0]
}
#[cfg(test)]
fn fit_limited(&self, graph: Size, fallback_width: f32) -> bool {
let available = self.fit_area(fallback_width);
self.fit && (graph.width * 0.05 > available.width || graph.height * 0.05 > available.height)
@@ -508,6 +602,22 @@ fn link(p: &Project, source: Id, target: Id, guard: &str) -> Command {
}
impl Draft {
fn command(&self, p: &Project) -> Result<Command, String> {
if self.source.engaged()
&& self.source_origin.as_ref().is_none_or(|(document, owner)| {
*document != p.id || p.elements.get(&self.owner) != Some(owner)
})
{
return Err("The original consumer document or owner changed. Cancel and reopen its form; your draft is retained.".into());
}
self.source.check_scope(
super::source_draft::Scope::Work { owner: self.owner },
self.target,
)?;
let preview = self.source.preview(p, self.target)?;
let command = self.consumer_command(preview.as_ref().unwrap_or(p))?;
self.source.combine(p, self.target, command)
}
fn consumer_command(&self, p: &Project) -> Result<Command, String> {
let owner = p
.elements
.get(&self.owner)
@@ -615,6 +725,8 @@ impl Draft {
});
let name = if self.name.trim().is_empty() {
default_name
} else if self.source.label(self.target).is_some() {
self.name.as_str()
} else {
self.name.trim()
};
@@ -866,7 +978,8 @@ mod contextual_routing;
struct Graph<'a> {
project: &'a Project,
interaction: Option<(Context, CanvasTool, bool)>,
scroll_offset: iced::Vector,
camera_pan: iced::Vector,
pending_insertion: Option<Insertion>,
nodes: Vec<GraphNode>,
edges: Vec<GraphEdge>,
ordered_paths: Vec<OrderedPath>,
@@ -1198,7 +1311,8 @@ impl<'a> Graph<'a> {
Self {
project: p,
interaction: None,
scroll_offset: iced::Vector::ZERO,
camera_pan: iced::Vector::ZERO,
pending_insertion: None,
nodes,
edges,
ordered_paths,
@@ -1298,6 +1412,7 @@ impl canvas::Program<Message> for Graph<'_> {
) -> Vec<canvas::Geometry> {
let mut f = Frame::new(renderer, bounds.size());
f.fill_rectangle(Point::ORIGIN, bounds.size(), Color::WHITE);
f.translate(self.camera_pan);
f.scale(self.scale);
for pass in PAINT_PASSES {
match pass {
@@ -1513,7 +1628,10 @@ impl canvas::Program<Message> for Graph<'_> {
cursor: mouse::Cursor,
) -> mouse::Interaction {
if cursor.position_in(bounds).is_some_and(|p| {
let p = Point::new(p.x / self.scale, p.y / self.scale);
let p = Point::new(
(p.x - self.camera_pan.x) / self.scale,
(p.y - self.camera_pan.y) / self.scale,
);
self.nodes.iter().any(|n| n.rect.contains(p))
|| self.ordered_paths.iter().any(|path| {
path.points.windows(2).any(|pair| {
@@ -1587,6 +1705,9 @@ mod tests {
}
fn branch(owner: Id, a: Id, b: Id, before: Id, after: Id) -> Draft {
Draft {
source: Default::default(),
invocation: Id::new_v4(),
source_origin: None,
insertion: Insertion::default(),
fixed: false,
browsers: Default::default(),
@@ -2043,7 +2164,6 @@ mod tests {
let accepted = app.document.snapshot();
assert!(app.behavior.prepare_pending(app.project()).is_err());
for (shelf, expected) in [
(Shelf::Tools, Some(Shelf::Tools)),
(Shelf::Selection, Some(Shelf::Selection)),
(Shelf::Selection, None),
] {
@@ -2060,10 +2180,12 @@ mod tests {
}
#[test]
fn process_fit_canvas_selection_ignores_retained_ordered_page_scroll() {
fn process_camera_selection_ignores_independent_details_scroll() {
let (p, owner, _, _, _, _) = fixture();
let mut state = State::default();
state.scrolled(scenario::ScrollArea::Canvas, iced::Vector::new(11., 19.));
state.camera(crate::behavior::canvas_workspace::Event::Pan(
iced::Vector::new(11., 19.),
));
state.scrolled(scenario::ScrollArea::Page, iced::Vector::new(0., 370.));
let context = Context {
document: p.id,
@@ -2074,16 +2196,17 @@ mod tests {
for scale in [0.45, 1.] {
let mut graph = Graph::new(&p, owner, owner);
graph.scale = scale;
graph.scroll_offset = state.canvas_offset();
graph.camera_pan = state.pan;
graph.interaction = Some((context, CanvasTool::Select, true));
let node = &graph.nodes[0];
let id = node.id;
let point = node.rect.center();
let bounds = Rectangle::new(Point::new(40., 100.), Size::new(800., 300.));
let screen = Point::new(
bounds.x + point.x * scale - 11.,
bounds.y + point.y * scale - 19.,
bounds.x + point.x * scale + 11.,
bounds.y + point.y * scale + 19.,
);
assert!(bounds.contains(screen));
let message = graph
.input(
&mut Input::default(),
@@ -33,7 +33,7 @@ pub enum Event {
Search(String),
Page(usize),
}
#[derive(Default, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub(super) struct Draft {
edit: Option<Id>,
@@ -608,7 +608,11 @@ mod tests {
"Authored endpoint is retained for explicit repair"
);
assert!(state.prepare_pending(&deleted).is_err());
state.remember(owner, reference);
state.remember(
owner,
reference,
super::super::direct::referenced(&p, owner, reference).unwrap(),
);
assert_eq!(state.back(&deleted), Some((owner, reference)));
assert!(state.selected_context.is_none());
state.draft = None;
+162 -45
View File
@@ -60,6 +60,7 @@ pub enum CanvasEvent {
Open(Context, Id),
Back(Context),
Zoom(Context, Option<f32>),
Camera(Context, u64, super::super::canvas_workspace::Event),
Focus(Context),
Viewport(Context, Size),
}
@@ -77,6 +78,7 @@ impl CanvasEvent {
| Self::Back(c)
| Self::AppendExchange(c)
| Self::Zoom(c, ..)
| Self::Camera(c, ..)
| Self::Focus(c)
| Self::Viewport(c, ..) => *c,
Self::Begin { context, .. }
@@ -86,6 +88,19 @@ impl CanvasEvent {
}
}
}
pub(super) struct ReturnFrame {
identity: Option<(Id, Id)>,
owner: Id,
occurrence: Id,
target: Id,
address: Option<oa_core::topology_context::ContextAddress>,
pan: iced::Vector,
zoom: Option<f32>,
fit: bool,
viewport: Option<Size>,
expanded: BTreeSet<Id>,
contextual_expanded: BTreeSet<OccurrenceEndpoint>,
}
impl State {
pub(crate) fn select_context(&mut self, address: oa_core::topology_context::ContextAddress) {
self.selected_context = Some(address);
@@ -138,35 +153,49 @@ impl State {
if self.identity != Some((document, owner)) {
self.identity = Some((document, owner));
self.tool = CanvasTool::Select;
self.pan = iced::Vector::ZERO;
self.zoom = Some(1.);
self.fit = false;
self.viewport = None;
self.expanded.clear();
self.scroll_offsets = [iced::Vector::ZERO; 2];
self.details_offset = iced::Vector::ZERO;
}
}
#[cfg(test)]
pub(crate) fn scroll_offset(&self) -> iced::Vector {
self.scroll_offsets[0] + self.scroll_offsets[1]
pub(crate) fn details_offset(&self) -> iced::Vector {
self.details_offset
}
pub(crate) fn scrolled(&mut self, area: scenario::ScrollArea, offset: iced::Vector) -> bool {
let index = match area {
scenario::ScrollArea::Canvas => 0,
scenario::ScrollArea::Page => 1,
};
let changed = self.scroll_offsets[index] != offset;
self.scroll_offsets[index] = offset;
pub(crate) fn scrolled(
&mut self,
scenario::ScrollArea::Page: scenario::ScrollArea,
offset: iced::Vector,
) -> bool {
if !offset.x.is_finite() || !offset.y.is_finite() || offset.x < 0. || offset.y < 0. {
return false;
}
let changed = self.details_offset != offset;
self.details_offset = offset;
changed
}
pub(crate) fn choose(&mut self, tool: CanvasTool) {
// Choosing how to edit must retain the user's current view.
self.tool = tool;
}
pub(crate) fn camera(&mut self, event: super::super::canvas_workspace::Event) -> bool {
super::super::canvas_workspace::change(&mut self.pan, &mut self.zoom, &mut self.fit, event)
}
pub(crate) fn zoom(&mut self, value: Option<f32>) {
self.fit = value.is_none();
self.zoom = value.map(|v| v.clamp(0.5, 2.));
self.zoom = value.map(|v| v.clamp(0.05, 3.));
}
pub(crate) fn viewport(&mut self, size: Size) -> bool {
if !size.width.is_finite()
|| !size.height.is_finite()
|| size.width <= 0.
|| size.height <= 0.
{
return false;
}
let changed = self.viewport != Some(size);
self.viewport = Some(size);
changed
@@ -188,26 +217,83 @@ impl State {
}
true
}
pub(crate) fn remember(&mut self, owner: Id, occurrence: Id) {
self.return_to
.push((owner, occurrence, self.selected_context.clone()));
pub(crate) fn remember(&mut self, owner: Id, occurrence: Id, target: Id) {
if self.return_to.len() == 32 {
self.return_to.remove(0);
}
self.return_to.push(ReturnFrame {
identity: self.identity,
owner,
occurrence,
target,
address: self.selected_context.clone(),
pan: self.pan,
zoom: self.zoom,
fit: self.fit,
viewport: self.viewport,
expanded: self.expanded.clone(),
contextual_expanded: self.contextual_expanded.clone(),
});
}
pub(crate) fn back(&mut self, p: &Project) -> Option<(Id, Id)> {
while let Some((owner, occurrence, address)) = self.return_to.pop() {
if is_contribution(p, owner, occurrence) {
self.selected_context = address.filter(|address| {
address.owner == owner
&& oa_core::topology_context::TopologyIndex::new(p)
.resolve(owner, &address.endpoint)
.is_ok()
while let Some(frame) = self.return_to.pop() {
if frame.identity.map(|i| i.0) != self.identity.map(|i| i.0) {
continue;
}
let valid = referenced(p, frame.owner, frame.occurrence) == Some(frame.target)
|| if let Some(address) = &frame.address {
oa_core::topology_context::TopologyIndex::new(p)
.resolve(frame.owner, &address.endpoint)
.is_ok_and(|r| r.defining_sequence == frame.target)
} else {
false
};
if valid && is_contribution(p, frame.owner, frame.occurrence) {
self.identity = frame.identity;
self.selected_context = frame.address.filter(|a| {
oa_core::topology_context::TopologyIndex::new(p)
.resolve(frame.owner, &a.endpoint)
.is_ok()
});
return Some((owner, occurrence));
self.pan = frame.pan;
self.zoom = frame.zoom;
self.fit = frame.fit;
self.viewport = frame.viewport;
self.expanded = frame.expanded;
self.contextual_expanded = frame.contextual_expanded;
return Some((frame.owner, frame.occurrence));
}
}
None
}
pub(crate) fn created_selection(&self, command: &Command) -> Option<Id> {
pub(crate) fn camera_zoom(&mut self, p: &Project, owner: Id, zoom: Option<f32>) {
let graph =
Graph::expanded_qualified(p, owner, owner, &self.expanded, &self.contextual_expanded);
let old = self.scale(graph.size, 500.);
if let Some(next) = zoom {
let size = self.viewport.unwrap_or(Size::new(500., 300.));
self.camera(super::super::canvas_workspace::Event::Zoom {
factor: next / old,
at: Point::new(size.width / 2., size.height / 2.),
base: old,
});
} else {
self.zoom(None);
let fitted = self.scale(graph.size, 500.);
self.zoom(Some(fitted));
self.pan = iced::Vector::ZERO;
}
}
pub(crate) fn created_selection(&self, p: &Project, command: &Command) -> Option<Id> {
let owner = self.pending_owner()?;
if self
.draft
.as_ref()
.is_some_and(|d| d.source.label(d.target).is_some())
&& self.prepare_pending(p).ok().as_ref() != Some(command)
{
return None;
}
if let Command::CreateRelation(r) | Command::EditRelation(_, r) = command
&& matches!(&r.body, RelationBody::ContextualLink { owner: caller, .. } if *caller == owner)
{
@@ -539,7 +625,7 @@ impl Graph<'_> {
{
*moved |= position.distance(*start) >= 3.;
}
mouse::Cursor::Available(*position + self.scroll_offset)
mouse::Cursor::Available(*position)
}
canvas::Event::Mouse(mouse::Event::WheelScrolled { .. } | mouse::Event::CursorLeft)
| canvas::Event::Window(
@@ -558,18 +644,19 @@ impl Graph<'_> {
}
canvas::Event::Mouse(
mouse::Event::ButtonPressed(_) | mouse::Event::ButtonReleased(_),
) => state
.pointer
.map_or(cursor, |p| mouse::Cursor::Available(p + self.scroll_offset)),
) => state.pointer.map_or(cursor, mouse::Cursor::Available),
canvas::Event::Window(iced::window::Event::RedrawRequested(_)) => {
state.pointer = cursor.position().map(|p| p - self.scroll_offset);
state.pointer = cursor.position();
cursor
}
_ => cursor,
};
let world = cursor
.position_in(bounds)
.map(|p| Point::new(p.x / self.scale, p.y / self.scale));
let world = cursor.position_in(bounds).map(|p| {
Point::new(
(p.x - self.camera_pan.x) / self.scale,
(p.y - self.camera_pan.y) / self.scale,
)
});
let publish = |event| Some(Action::publish(Message::TopologyCanvas(event)).and_capture());
match event {
canvas::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) if visible => {
@@ -583,7 +670,7 @@ impl Graph<'_> {
if !editable {
return None;
}
let start = cursor.position()? - self.scroll_offset;
let start = cursor.position()?;
match tool {
CanvasTool::Add(..) => {
state.press = Some(Press::Add {
@@ -722,15 +809,35 @@ impl Graph<'_> {
let Some((context, tool, editable)) = self.interaction else {
return;
};
if let Some(insertion) = self.pending_insertion {
if let Some(edge) = insertion
.link
.and_then(|id| self.edges.iter().find(|e| e.id == id))
{
draw_arrow(f, &edge.points, style::TEAL, 3.);
} else if let Some(node) = insertion
.before
.and_then(|id| self.nodes.iter().find(|n| n.id == id))
{
f.stroke(
&Path::rounded_rectangle(node.rect.position(), node.rect.size(), 10.into()),
Stroke::default().with_color(style::TEAL).with_width(3.),
);
}
}
if !editable {
return;
}
let point = state
.pointer
.map(|p| p + self.scroll_offset)
.or_else(|| cursor.position())
.filter(|p| bounds.contains(*p))
.map(|p| Point::new((p.x - bounds.x) / self.scale, (p.y - bounds.y) / self.scale));
.map(|p| {
Point::new(
(p.x - bounds.x - self.camera_pan.x) / self.scale,
(p.y - bounds.y - self.camera_pan.y) / self.scale,
)
});
let live =
state.context == Some(context) && state.tool == tool && state.bounds == Some(bounds);
if live && let Some(source) = state.source {
@@ -845,12 +952,7 @@ impl Graph<'_> {
}
impl State {
pub(crate) fn selection_offset(
&self,
p: &Project,
owner: Id,
id: Id,
) -> Option<scrollable::AbsoluteOffset> {
pub(crate) fn reveal_selection(&mut self, p: &Project, owner: Id, id: Id) -> bool {
let graph =
Graph::expanded_qualified(p, owner, id, &self.expanded, &self.contextual_expanded);
let scale = self.scale(graph.size, 500.);
@@ -862,10 +964,25 @@ impl State {
self.qualified_selection(p, owner)
.map_or(n.rect, |a| n.endpoint_rect(&a.endpoint))
})
.or_else(|| graph.edges.iter().find(|e| e.id == id).map(|e| e.label))?;
Some(scrollable::AbsoluteOffset {
x: (rect.x * scale - 12.).max(0.),
y: (rect.y * scale - 12.).max(0.),
})
.or_else(|| graph.edges.iter().find(|e| e.id == id).map(|e| e.label));
let Some(rect) = rect else {
return false;
};
let viewport = self.viewport.unwrap_or(Size::new(500., 300.));
let x = rect.x * scale + self.pan.x;
let y = rect.y * scale + self.pan.y;
let delta = iced::Vector::new(
if x < 12. || x + rect.width * scale > viewport.width - 12. {
12. - x
} else {
0.
},
if y < 12. || y + rect.height * scale > viewport.height - 12. {
12. - y
} else {
0.
},
);
self.camera(super::super::canvas_workspace::Event::Pan(delta))
}
}
@@ -540,7 +540,7 @@ fn send(
input,
&canvas::Event::Mouse(event),
bounds,
mouse::Cursor::Available(point + graph.scroll_offset),
mouse::Cursor::Available(point),
)
.and_then(|a| a.into_inner().0)
}
@@ -554,16 +554,20 @@ fn position(graph: &Graph<'_>, id: Id) -> Point {
.center()
}
#[test]
fn direct_process_production_click_click_drag_insert_and_scroll_coordinates() {
fn direct_process_production_click_click_drag_insert_and_camera_coordinates() {
let f = fixture(Layer::Oa);
for drag in [false, true] {
let mut graph = graph(&f, CanvasTool::Connect);
graph.scroll_offset = iced::Vector::new(35., 90.);
let bounds = Rectangle::new(Point::new(10., 20.), graph.size);
graph.camera_pan = iced::Vector::new(35., 90.);
let bounds = Rectangle::new(
Point::new(10., 20.),
Size::new(graph.size.width + 70., graph.size.height + 180.),
);
let screen =
|id| position(&graph, id) + iced::Vector::new(bounds.x, bounds.y) - graph.scroll_offset;
|id| position(&graph, id) + iced::Vector::new(bounds.x, bounds.y) + graph.camera_pan;
let a = screen(f.nodes[0]);
let b = screen(f.nodes[2]);
assert!(bounds.contains(a) && bounds.contains(b));
let mut input = Input::default();
send(
&graph,
@@ -732,7 +736,7 @@ fn direct_process_readable_default_fit_expansion_and_bounded_choices() {
state.zoom(None);
assert!(state.scale(Size::new(3262., 220.), 500.) < 0.2);
state.zoom(Some(0.1));
assert_eq!(state.scale(Size::new(3262., 220.), 500.), 0.5);
assert_eq!(state.scale(Size::new(3262., 220.), 500.), 0.1);
state.enter(Id::new_v4(), f.owner);
assert_eq!(state.scale(Size::new(3262., 220.), 500.), 1.);
let mut all = Vec::new();
@@ -1331,7 +1335,9 @@ fn direct_process_palette_preserves_fit_and_manual_camera() {
let document = Id::new_v4();
state.enter(document, f.owner);
state.viewport(Size::new(500., 300.));
state.scrolled(scenario::ScrollArea::Canvas, iced::Vector::new(35., 90.));
state.camera(super::super::canvas_workspace::Event::Pan(
iced::Vector::new(35., 90.),
));
state.scrolled(scenario::ScrollArea::Page, iced::Vector::new(0., 120.));
for camera in [None, Some(0.75), Some(1.5)] {
state.zoom(camera);
@@ -1353,90 +1359,16 @@ fn direct_process_palette_preserves_fit_and_manual_camera() {
"{layer:?} {camera:?} {tool:?}"
);
assert_eq!(state.fit, camera.is_none());
assert_eq!(state.scroll_offset(), iced::Vector::new(35., 210.));
assert_eq!(state.pan, iced::Vector::new(35., 90.));
assert_eq!(state.details_offset(), iced::Vector::new(0., 120.));
state.enter(document, f.owner);
assert_eq!(state.scale(size, 500.), before);
}
}
state.enter(Id::new_v4(), f.owner);
assert_eq!(state.scale(Size::new(690., 250.), 500.), 1.);
assert_eq!(state.scroll_offset(), iced::Vector::ZERO);
}
}
#[test]
fn direct_process_pending_form_diff_retains_the_canvas_scroll_state() {
use iced::advanced::widget::{Tree, tree};
use std::rc::Rc;
fn path_to(tree: &Tree, tag: tree::Tag) -> Option<Vec<usize>> {
if tree.tag == tag {
return Some(Vec::new());
}
tree.children.iter().enumerate().find_map(|(index, child)| {
let mut path = path_to(child, tag)?;
path.insert(0, index);
Some(path)
})
}
fn at<'a>(mut tree: &'a mut Tree, path: &[usize]) -> &'a mut Tree {
for &index in path {
tree = &mut tree.children[index];
}
tree
}
for layer in [Layer::Oa, Layer::Sa] {
for size in [Size::new(1440., 900.), Size::new(1100., 700.)] {
for tool in [Tool::Control, Tool::Branch, Tool::Connect] {
let f = fixture(layer);
let mut app = App::with_project(f.p.clone());
let _ = app.update(Message::Select(f.nodes[1]));
let _ = app.update(Message::Page(crate::app::Page::Behavior));
app.window_size = size;
let original = app.document.snapshot();
let graph: Element<'_, Message> =
canvas::Canvas::new(Graph::new(app.project(), f.owner, f.nodes[1])).into();
let canvas_tag = Tree::new(graph.as_widget()).tag;
drop(graph);
let scroll: Element<'_, Message> = scrollable(text("")).into();
let scroll_tag = Tree::new(scroll.as_widget()).tag;
let mut tree = Tree::new(view::view(&app, f.owner, Column::new()).as_widget());
let mut scroll_path = path_to(&tree, canvas_tag).unwrap();
scroll_path.pop();
assert_eq!(at(&mut tree, &scroll_path).tag, scroll_tag);
// Scrollable::diff treats its local state as opaque. A retained marker
// proves the real Iced diff preserves the state that owns the offsets.
// No layout or input operation runs on this test-only marker state.
let marker = Rc::new(());
at(&mut tree, &scroll_path).state = tree::State::new(Rc::clone(&marker));
let _ = app.update(Message::Behavior(super::super::Event::Topology(
Event::Start(f.owner, tool, Some(f.nodes[1])),
)));
assert!(app.behavior.has_pending());
tree.diff(view::view(&app, f.owner, Column::new()).as_widget());
assert_eq!(
Rc::strong_count(&marker),
2,
"form open: {layer:?} {size:?} {tool:?}"
);
let mut current_path = path_to(&tree, canvas_tag).unwrap();
current_path.pop();
assert_eq!(current_path, scroll_path);
assert!(Rc::ptr_eq(
at(&mut tree, &scroll_path).state.downcast_ref::<Rc<()>>(),
&marker,
));
let _ = app.update(Message::Behavior(super::super::Event::Topology(
Event::Cancel,
)));
tree.diff(view::view(&app, f.owner, Column::new()).as_widget());
assert_eq!(
Rc::strong_count(&marker),
2,
"form cancel: {layer:?} {size:?} {tool:?}"
);
assert!(std::sync::Arc::ptr_eq(&app.document.snapshot(), &original));
}
}
assert_eq!(state.pan, iced::Vector::ZERO);
assert_eq!(state.details_offset(), iced::Vector::ZERO);
}
}
@@ -1447,15 +1379,18 @@ fn direct_process_scaled_control_and_link_gestures_use_visible_positions() {
for scale in [0.15, 0.6898551, 1.5] {
let mut graph = graph(&f, CanvasTool::Add(Tool::Control, ControlKind::Choice));
graph.scale = scale;
graph.scroll_offset = iced::Vector::new(11., 19.);
graph.camera_pan = iced::Vector::new(11., 19.);
let bounds = Rectangle::new(
Point::new(40., 70.),
Size::new(graph.size.width * scale, graph.size.height * scale),
Size::new(
graph.size.width * scale + 22.,
graph.size.height * scale + 38.,
),
);
let screen = |world: Point| {
Point::new(
world.x * scale + bounds.x - 11.,
world.y * scale + bounds.y - 19.,
world.x * scale + bounds.x + 11.,
world.y * scale + bounds.y + 19.,
)
};
let edge = &graph.edges[0];
@@ -1463,6 +1398,7 @@ fn direct_process_scaled_control_and_link_gestures_use_visible_positions() {
(edge.points[0].x + edge.points[1].x) / 2.,
edge.points[0].y,
));
assert!(bounds.contains(point));
let mut input = Input::default();
send(
&graph,
@@ -1493,6 +1429,7 @@ fn direct_process_scaled_control_and_link_gestures_use_visible_positions() {
let mut input = Input::default();
let a = screen(position(&graph, f.nodes[0]));
let b = screen(position(&graph, f.nodes[2]));
assert!(bounds.contains(a) && bounds.contains(b));
send(
&graph,
&mut input,
File diff suppressed because it is too large Load Diff
+394 -463
View File
@@ -66,107 +66,7 @@ pub(super) fn view<'a>(app: &'a App, owner: Id, out: Column<'a, Message>) -> Ele
let pending = state.draft.is_some();
let ordered = !legacy_order(p, owner).is_empty();
let selected = is_contribution(p, owner, app.selected).then_some(app.selected);
let choose = |tool| Message::TopologyCanvas(CanvasEvent::Choose(context, tool));
let mut controls = Column::new().spacing(6);
if !pending {
controls = controls
.push(
row![
button("Select / reorder")
.style(if state.tool == CanvasTool::Select {
button::primary
} else {
style::nav
})
.on_press(choose(CanvasTool::Select)),
button("Work")
.style(
if state.tool == CanvasTool::Add(Tool::Work, ControlKind::Choice) {
button::primary
} else {
style::nav
}
)
.on_press(choose(CanvasTool::Add(Tool::Work, ControlKind::Choice))),
button("Exchange")
.style(
if state.tool == CanvasTool::Add(Tool::Exchange, ControlKind::Choice) {
button::primary
} else {
style::nav
}
)
.on_press(choose(CanvasTool::Add(Tool::Exchange, ControlKind::Choice))),
button("Reuse")
.style(
if state.tool == CanvasTool::Add(Tool::Reuse, ControlKind::Choice) {
button::primary
} else {
style::nav
}
)
.on_press(choose(CanvasTool::Add(Tool::Reuse, ControlKind::Choice)))
]
.spacing(6),
)
.push(
row![
pick_list(
vec![
ControlKind::Choice,
ControlKind::Merge,
ControlKind::Fork,
ControlKind::Join,
ControlKind::Loop
]
.into_iter()
.map(ControlChoice)
.collect::<Vec<_>>(),
if let CanvasTool::Add(Tool::Control, kind) = state.tool {
Some(ControlChoice(kind))
} else {
None
},
move |c| Message::TopologyCanvas(CanvasEvent::Choose(
context,
CanvasTool::Add(Tool::Control, c.0)
))
)
.placeholder("Draw control…")
.text_size(12)
.padding(8),
button("Sequence link")
.style(if state.tool == CanvasTool::Connect {
button::primary
} else {
style::nav
})
.on_press(choose(CanvasTool::Connect)),
button("Two paths…").on_press(event(Event::Start(
owner,
Tool::Branch,
selected
)))
]
.spacing(6),
);
}
controls=controls.push(label(if pending{"Pending contribution · apply or cancel below"}else if ordered{"Ordered exchange path · click a path to insert or empty canvas to continue; the edit preserves its order"}else{match state.tool{
CanvasTool::Select=>"Select local contributions and paths · drag path endpoints to reconnect · ↕ reorders within a column",
CanvasTool::Connect=>"Sequence link · click or drag from preceding to following contribution · Escape cancels",
CanvasTool::Add(..)=>"Choose a highlighted path or contribution position · existing work stays shared · Escape cancels"}}));
if !pending && ordered {
controls = controls.push(button("Append exchange…").on_press(Message::TopologyCanvas(
CanvasEvent::AppendExchange(context),
)));
let commands = preserved_order(p, owner, None);
if !commands.is_empty() {
controls = controls.push(
button("Edit path links and guards")
.on_press(Message::Command(unique_batch(p, commands))),
);
}
}
let clean = !app.behavior.has_pending() && !app.draft_dirty() && !app.source_dirty();
// A fixed sibling keeps Iced's canvas scroll state through selection/form changes.
let mut navigation = column![].spacing(6);
if let Some(Relation {
@@ -211,11 +111,6 @@ pub(super) fn view<'a>(app: &'a App, owner: Id, out: Column<'a, Message>) -> Ele
);
}
}
if !pending {
controls = controls.push(button("Qualified link / assembly…").on_press(event(
Event::Start(owner, Tool::Contextual, Some(app.selected)),
)));
}
if let Some(id) = selected {
let mut actions =
row![button("Contribution properties").on_press(Message::EditProperties(id))]
@@ -256,27 +151,79 @@ pub(super) fn view<'a>(app: &'a App, owner: Id, out: Column<'a, Message>) -> Ele
button("Back to caller").on_press(Message::TopologyCanvas(CanvasEvent::Back(context))),
);
}
let diagram = {
let mut graph = Graph::expanded_qualified(
p,
owner,
app.selected,
&state.expanded,
&state.contextual_expanded,
navigation = navigation
.push(
button("Two paths...").on_press_maybe(clean.then_some(event(Event::Start(
owner,
Tool::Branch,
selected,
)))),
)
.push(
button("Qualified link / assembly...").on_press_maybe(clean.then_some(event(
Event::Start(owner, Tool::Contextual, Some(app.selected)),
))),
);
graph.scale = state.scale(graph.size, (app.window_size.width - 600.).max(200.));
graph.interaction = Some((
context,
state.tool,
!app.behavior.has_pending() && !app.draft_dirty() && !app.source_dirty(),
));
// Explicit graphs no longer have a scrolling page ancestor. A retained
// offset from an ordered view must not distort their pointer coordinates.
graph.scroll_offset = state.canvas_offset();
let scale = graph.scale;
let mut header = column![
row![
out.width(Fill),
if ordered {
navigation = navigation.push(button("Append exchange...").on_press_maybe(clean.then_some(
Message::TopologyCanvas(CanvasEvent::AppendExchange(context)),
)));
let commands = preserved_order(p, owner, None);
if !commands.is_empty() {
navigation = navigation.push(
button("Edit path links and guards")
.on_press_maybe(clean.then_some(Message::Command(unique_batch(p, commands)))),
);
}
}
let mut graph = Graph::expanded_qualified(
p,
owner,
app.selected,
&state.expanded,
&state.contextual_expanded,
);
graph.scale = state.scale(graph.size, (app.window_size.width - 600.).max(200.));
graph.camera_pan = state.pan;
graph.pending_insertion = state
.draft
.as_ref()
.filter(|d| d.owner == owner && d.fixed)
.map(|d| d.insertion);
graph.interaction = Some((context, state.tool, clean));
let scale = graph.scale;
let scope = super::super::canvas_workspace::Scope {
document: context.document,
owner,
revision: context.revision,
epoch: app.camera_epoch,
};
let keyboard = clean && super::super::canvas_workspace::keyboard_enabled(app);
let camera = move |e| Message::TopologyCanvas(CanvasEvent::Camera(context, scope.epoch, e));
let canvas = container(
iced::widget::sensor(
super::super::canvas_workspace::Viewport::new(
canvas::Canvas::new(graph).width(Fill).height(Fill),
scope,
scale,
keyboard,
camera,
)
.pan_background(state.tool == CanvasTool::Select),
)
.key((context.document, owner))
.on_show(move |size| Message::TopologyCanvas(CanvasEvent::Viewport(context, size)))
.on_resize(move |size| Message::TopologyCanvas(CanvasEvent::Viewport(context, size))),
)
.id("process-viewport")
.width(Fill)
.height(Fill)
.style(style::card)
.into();
let header = column![
row![
out.width(Fill),
super::super::canvas_workspace::Control::new(
button(
text(if state.focused {
"Restore panels"
@@ -285,368 +232,352 @@ pub(super) fn view<'a>(app: &'a App, owner: Id, out: Column<'a, Message>) -> Ele
})
.size(12)
)
.on_press(Message::TopologyCanvas(CanvasEvent::Focus(context)))
.padding(6)
]
.spacing(6)
.align_y(iced::Center),
row![
button(text("Tools").size(12))
.padding(6)
.style(if state.shelf == Some(Shelf::Tools) {
button::primary
} else {
style::nav
})
.on_press(event(Event::Shelf(Shelf::Tools))),
button(text("Selection details").size(12))
.padding(6)
.style(if state.shelf == Some(Shelf::Selection) {
button::primary
} else {
style::nav
})
.on_press(event(Event::Shelf(Shelf::Selection))),
button(text("Fit overview").size(12))
.padding(6)
.on_press(Message::TopologyCanvas(CanvasEvent::Zoom(context, None))),
button(text("−").size(12))
.padding(6)
.on_press(Message::TopologyCanvas(CanvasEvent::Zoom(
context,
Some(scale / 1.25)
))),
label(format!("{:.0}%", scale * 100.)),
button(text("+").size(12))
.padding(6)
.on_press(Message::TopologyCanvas(CanvasEvent::Zoom(
context,
Some(scale * 1.25)
))),
button(text("100%").size(12))
.padding(6)
.on_press(Message::TopologyCanvas(CanvasEvent::Zoom(
context,
Some(1.)
)))
]
.spacing(6)
.wrap(),
.on_press(super::super::canvas_workspace::intent(
scope,
"Panels",
Message::TopologyCanvas(CanvasEvent::Focus(context))
)),
scope,
"Panels",
keyboard,
Some(super::super::canvas_workspace::intent(
scope,
"Panels",
Message::TopologyCanvas(CanvasEvent::Focus(context))
)),
camera
)
]
.spacing(6);
if state.fit_limited(graph.size, (app.window_size.width - 600.).max(200.)) {
header = header
.push(text("Minimum zoom reached · scroll to see the remaining graph.").size(11));
}
if graph.nodes.is_empty() {
controls = controls.push(label(
"Choose Work, Exchange, Reuse or a control, then click the canvas to start.",
));
}
let size = Size::new(graph.size.width * scale, graph.size.height * scale);
let canvas = scrollable(
canvas::Canvas::new(graph)
.width(size.width)
.height(size.height),
)
.id("process-canvas")
.direction(scrollable::Direction::Both {
vertical: scrollable::Scrollbar::default(),
horizontal: scrollable::Scrollbar::default(),
})
.width(Fill)
.height(Fill);
let canvas = container(
iced::widget::sensor(super::super::scenario_scroll::observe_topology(
.spacing(6),
super::super::canvas_workspace::controls(
scope,
keyboard,
scale,
event(Event::Shelf(Shelf::Selection)),
clean.then_some(Message::TopologyCanvas(CanvasEvent::Camera(
context,
scenario::ScrollArea::Canvas,
canvas,
))
.on_show(move |size| Message::TopologyCanvas(CanvasEvent::Viewport(context, size)))
.on_resize(move |size| Message::TopologyCanvas(CanvasEvent::Viewport(context, size))),
scope.epoch,
super::super::canvas_workspace::Event::Focus
))),
move |zoom| Message::TopologyCanvas(CanvasEvent::Zoom(context, zoom)),
camera
)
.padding(8)
.width(Fill)
.height(Fill)
.style(style::card);
let details: Element<'_, Message> = match state.shelf {
Some(Shelf::Tools) => controls.into(),
_ => navigation.push(text("Full qualified identities and definition actions appear here for the selected contribution or link.").size(11)).into(),
};
explicit_pane(
header.into(),
details,
canvas.into(),
state.shelf.is_some(),
pending,
)
};
]
.spacing(4);
let entries = [
("S", "Select contributions or paths", CanvasTool::Select),
(
"W",
"Work: click a path or empty space",
CanvasTool::Add(Tool::Work, ControlKind::Choice),
),
(
"E",
"Exchange: click a path or empty space",
CanvasTool::Add(Tool::Exchange, ControlKind::Choice),
),
(
"ref",
"Reuse process / chain",
CanvasTool::Add(Tool::Reuse, ControlKind::Choice),
),
(
"OR",
"Choice",
CanvasTool::Add(Tool::Control, ControlKind::Choice),
),
(
"M",
"Merge",
CanvasTool::Add(Tool::Control, ControlKind::Merge),
),
(
"AND",
"Fork",
CanvasTool::Add(Tool::Control, ControlKind::Fork),
),
(
"J",
"Join",
CanvasTool::Add(Tool::Control, ControlKind::Join),
),
(
"IT",
"Loop",
CanvasTool::Add(Tool::Control, ControlKind::Loop),
),
(
"->",
"Sequence link: click or drag between contributions",
CanvasTool::Connect,
),
];
let palette = super::super::canvas_workspace::palette(
scope,
keyboard,
camera,
entries.into_iter().map(|(glyph, label, choice)| {
(
glyph,
label,
choice == state.tool,
clean.then_some(Message::TopologyCanvas(CanvasEvent::Choose(
context, choice,
))),
)
}),
);
let details = scrollable(navigation.push(text("Layout is derived. The reorder handle changes display order within its column; it does not author a position.").size(11)))
.id("process-details").width(Fill).height(Fill).into();
let mut form_panel: Element<'_, Message> = iced::widget::Space::new().height(0).into();
if let Some(d) = state.draft.as_ref().filter(|d| d.owner == owner) {
let mut form = column![text(d.tool.title()).size(16)].spacing(8);
if let Some(contextual) = &d.contextual {
form = form
.push(
text_input("Link name (retain current when blank)", &d.name)
.on_input(|v| event(Event::Name(v)))
.padding(8)
.size(12),
)
.push(super::contextual::view(p, owner, contextual));
if let Some(child) = d.source.view(p) {
form_panel = column![
text(format!(
"Consumer name: {}",
crate::views::short(&d.name, 60)
))
.size(12),
child.map(|e| event(Event::Source(e)))
]
.spacing(6)
.height(Fill)
.into();
} else {
if let Some(link) = d.insertion.link.and_then(|id| p.relations.get(&id)) {
let guard = if let RelationBody::SequenceLink { guard, .. } = &link.body {
guard.as_str()
} else {
""
};
form = form.push(label(format!(
"Insert on {} → {} · preserve path identity and condition [{}]",
p.named(link.source()),
p.named(link.target()),
guard
)));
} else if d.fixed && ordered {
form = form.push(label(match (d.before, d.after) {
(Some(before), Some(after)) => format!(
"Insert between {} → {} · preserve the ordered path",
p.named(before),
p.named(after)
),
(Some(before), None) => format!("Continue after {}", p.named(before)),
(None, Some(after)) => format!("Insert before {}", p.named(after)),
_ => "Start the ordered path".into(),
}));
} else if d.fixed
&& d.tool == Tool::Exchange
&& let Some(before) = d.before
{
form = form.push(label(format!(
"Append after {} → new exchange · Create adds an explicit sequence link",
p.named(before)
)));
} else if d.fixed && d.tool != Tool::Connect {
form = form.push(label(
d.insertion
.before
.map_or("Append an unconnected contribution".into(), |id| {
format!("Unconnected contribution before {}", p.named(id))
}),
));
}
if !legacy_order(p, owner).is_empty() {
form = form.push(label(
let mut form = column![text(d.tool.title()).size(16)].spacing(8);
if let Some(contextual) = &d.contextual {
form = form
.push(
text_input("Link name (retain current when blank)", &d.name)
.on_input(|v| event(Event::Name(v)))
.padding(8)
.size(12),
)
.push(super::contextual::view(p, owner, contextual));
} else {
if let Some(link) = d.insertion.link.and_then(|id| p.relations.get(&id)) {
let guard = if let RelationBody::SequenceLink { guard, .. } = &link.body {
guard.as_str()
} else {
""
};
form = form.push(label(format!(
"Insert on {} → {} · preserve path identity and condition [{}]",
p.named(link.source()),
p.named(link.target()),
guard
)));
} else if d.fixed && ordered {
form = form.push(label(match (d.before, d.after) {
(Some(before), Some(after)) => format!(
"Insert between {} → {} · preserve the ordered path",
p.named(before),
p.named(after)
),
(Some(before), None) => format!("Continue after {}", p.named(before)),
(None, Some(after)) => format!("Insert before {}", p.named(after)),
_ => "Start the ordered path".into(),
}));
} else if d.fixed
&& d.tool == Tool::Exchange
&& let Some(before) = d.before
{
form = form.push(label(format!(
"Append after {} → new exchange · Create adds an explicit sequence link",
p.named(before)
)));
} else if d.fixed && d.tool != Tool::Connect {
form = form.push(label(
d.insertion
.before
.map_or("Append an unconnected contribution".into(), |id| {
format!("Unconnected contribution before {}", p.named(id))
}),
));
}
if d.tool == Tool::Work {
if let Some(title) = d.source.label(d.target) {
form = form.push(label(title)).push(label("Create commits the new work and contribution together at the chosen path position."));
}
form = form.push(button("Create new source…").on_press(event(Event::Source(
super::super::source_draft::Event::Open(d.invocation),
))));
}
if !legacy_order(p, owner).is_empty() {
form = form.push(label(
"This edit preserves the ordered path as explicit links in the same undo step.",
));
}
if matches!(
d.tool,
Tool::Work | Tool::Exchange | Tool::Reuse | Tool::Branch
) {
let choices = targets(p, owner, d.tool)
.into_iter()
.filter(|c| compatible_source(p, d, c.id))
.collect();
if d.tool == Tool::Branch {
form = form.push(label("First path work"));
}
form = form.push(picker(
choices,
d,
0,
d.target,
if matches!(
d.tool,
Tool::Work | Tool::Exchange | Tool::Reuse | Tool::Branch
) {
let choices = targets(p, owner, d.tool)
.into_iter()
.filter(|c| compatible_source(p, d, c.id))
.collect();
if d.tool == Tool::Branch {
"Choose first path work…"
form = form.push(label("First path work"));
}
form = form.push(picker(
choices,
d,
0,
d.target,
if d.tool == Tool::Branch {
"Choose first path work…"
} else {
"Choose existing source…"
},
Event::Target,
));
}
if d.tool != Tool::Connect {
form = form.push(
text_input("Contribution name (defaults to source)", &d.name)
.on_input(|v| event(Event::Name(v)))
.padding(8)
.size(12),
);
}
if matches!(d.tool, Tool::Control | Tool::Branch) {
let kinds = if d.tool == Tool::Branch {
vec![ControlKind::Choice, ControlKind::Fork]
} else {
"Choose existing source…"
},
Event::Target,
));
}
if d.tool != Tool::Connect {
form = form.push(
text_input("Contribution name (defaults to source)", &d.name)
.on_input(|v| event(Event::Name(v)))
vec![
ControlKind::Choice,
ControlKind::Merge,
ControlKind::Fork,
ControlKind::Join,
ControlKind::Loop,
]
};
form = form.push(
pick_list(
kinds.into_iter().map(ControlChoice).collect::<Vec<_>>(),
Some(ControlChoice(d.control)),
|v| event(Event::Control(v)),
)
.padding(8)
.text_size(12),
);
}
if d.tool == Tool::Branch {
form = form.push(label("Second path work")).push(picker(
targets(p, owner, Tool::Work),
d,
1,
d.alternate,
"Second path work…",
Event::Alternate,
));
if d.control != ControlKind::Fork {
form = form
.push(label("First path guard"))
.push(
text_input("First path guard", &d.guard)
.on_input(|v| event(Event::Guard(v)))
.padding(8),
)
.push(label("Second path guard"))
.push(
text_input("Second path guard (e.g. else)", &d.alternate_guard)
.on_input(|v| event(Event::AlternateGuard(v)))
.padding(8),
);
}
}
if d.fixed {
if let (Some(a), Some(b)) = (d.before, d.after) {
form = form.push(label(format!("{} → {}", p.named(a), p.named(b))));
}
} else {
form = form
.push(label("Preceding contribution"))
.push(picker(
contributions(p, owner),
d,
2,
d.before,
"After contribution…",
Event::Before,
))
.push(button("Clear preceding").on_press(event(Event::ClearBefore)))
.push(label("Following contribution"))
.push(picker(
contributions(p, owner),
d,
3,
d.after,
"Before contribution…",
Event::After,
))
.push(button("Clear following").on_press(event(Event::ClearAfter)));
}
if d.tool != Tool::Branch
&& d.insertion.link.is_none()
&& (d.before.is_some() || d.tool == Tool::Connect)
{
form = form.push(
text_input(
"Path condition (empty remains a Choice / Loop warning)",
&d.guard,
)
.on_input(|v| event(Event::Guard(v)))
.padding(8)
.size(12),
);
);
}
}
if matches!(d.tool, Tool::Control | Tool::Branch) {
let kinds = if d.tool == Tool::Branch {
vec![ControlKind::Choice, ControlKind::Fork]
let valid = match d.command(p) {
Ok(_) => true,
Err(problem) => {
form = form.push(label(problem));
false
}
};
let actions = row![
button(if d.contextual.is_some() {
"Apply link"
} else {
vec![
ControlKind::Choice,
ControlKind::Merge,
ControlKind::Fork,
ControlKind::Join,
ControlKind::Loop,
]
};
form = form.push(
pick_list(
kinds.into_iter().map(ControlChoice).collect::<Vec<_>>(),
Some(ControlChoice(d.control)),
|v| event(Event::Control(v)),
)
.padding(8)
.text_size(12),
);
}
if d.tool == Tool::Branch {
form = form.push(label("Second path work")).push(picker(
targets(p, owner, Tool::Work),
d,
1,
d.alternate,
"Second path work…",
Event::Alternate,
));
if d.control != ControlKind::Fork {
form = form
.push(label("First path guard"))
.push(
text_input("First path guard", &d.guard)
.on_input(|v| event(Event::Guard(v)))
.padding(8),
)
.push(label("Second path guard"))
.push(
text_input("Second path guard (e.g. else)", &d.alternate_guard)
.on_input(|v| event(Event::AlternateGuard(v)))
.padding(8),
);
}
}
if d.fixed {
if let (Some(a), Some(b)) = (d.before, d.after) {
form = form.push(label(format!("{} → {}", p.named(a), p.named(b))));
}
} else {
form = form
.push(label("Preceding contribution"))
.push(picker(
contributions(p, owner),
d,
2,
d.before,
"After contribution…",
Event::Before,
))
.push(button("Clear preceding").on_press(event(Event::ClearBefore)))
.push(label("Following contribution"))
.push(picker(
contributions(p, owner),
d,
3,
d.after,
"Before contribution…",
Event::After,
))
.push(button("Clear following").on_press(event(Event::ClearAfter)));
}
if d.tool != Tool::Branch
&& d.insertion.link.is_none()
&& (d.before.is_some() || d.tool == Tool::Connect)
{
form = form.push(
text_input(
"Path condition (empty remains a Choice / Loop warning)",
&d.guard,
)
.on_input(|v| event(Event::Guard(v)))
.padding(8)
.size(12),
);
}
"Create"
})
.on_press_maybe(valid.then(|| event(Event::Submit))),
button("Cancel").on_press(event(Event::Cancel))
]
.spacing(7);
form_panel = editor_panel(d.invocation, actions.into(), form.into());
}
let valid = match d.command(p) {
Ok(_) => true,
Err(problem) => {
form = form.push(label(problem));
false
}
};
let actions = row![
button(if d.contextual.is_some() {
"Apply link"
} else {
"Create"
})
.on_press_maybe(valid.then(|| event(Event::Submit))),
button("Cancel").on_press(event(Event::Cancel))
]
.spacing(7);
form_panel = editor_panel(actions.into(), form.into());
}
panes(diagram, form_panel, pending)
}
/// A permanent three-slot tree: variable controls never remount the canvas.
/// The shelf scrolls independently; the canvas receives actual remaining height.
fn explicit_pane<'a, R: iced::advanced::text::Renderer + 'a>(
header: iced::Element<'a, Message, iced::Theme, R>,
details: iced::Element<'a, Message, iced::Theme, R>,
canvas: iced::Element<'a, Message, iced::Theme, R>,
show_details: bool,
pending: bool,
) -> iced::Element<'a, Message, iced::Theme, R> {
// Column::push drops Fixed(0) children. from_vec retains the closed shelf
// slot so its disclosure cannot move/remount the following canvas tree.
Column::from_vec(vec![
container(header).width(Fill).into(),
scrollable(details)
.id("process-details")
.width(Fill)
.height(if show_details {
if pending { 64. } else { 112. }
} else {
0.
})
.into(),
container(canvas).width(Fill).height(Fill).into(),
])
.spacing(6)
.padding(8)
.width(Fill)
.height(Fill)
.into()
}
/// Keep the diagram mounted while an independent editor uses the remaining
/// workspace height. The editor must never be clipped by the page scrollable.
fn panes<'a, R: iced::advanced::Renderer + 'a>(
diagram: iced::Element<'a, Message, iced::Theme, R>,
form: iced::Element<'a, Message, iced::Theme, R>,
pending: bool,
) -> iced::Element<'a, Message, iced::Theme, R> {
column![
container(diagram).width(Fill).height(if pending {
iced::Length::FillPortion(1)
} else {
Fill
}),
container(form).width(Fill).height(if pending {
iced::Length::FillPortion(1)
} else {
iced::Length::Shrink
}),
]
.width(Fill)
.height(Fill)
.into()
let editor = if pending {
Some(form_panel)
} else {
state.shelf.map(|_| details)
};
super::super::canvas_workspace::host(header.into(), palette, canvas, editor)
}
fn editor_panel<'a, R: iced::advanced::text::Renderer + 'a>(
session: Id,
actions: iced::Element<'a, Message, iced::Theme, R>,
fields: iced::Element<'a, Message, iced::Theme, R>,
) -> iced::Element<'a, Message, iced::Theme, R> {
container(
container(column![actions, scrollable(fields).id("process-form").height(Fill)].spacing(8))
.width(Fill)
.height(Fill)
.padding(12)
.style(style::card),
container(
column![
container(actions).id("process-actions"),
super::super::form_session::reset_on_change(
session,
scrollable(fields).id("process-form").height(Fill)
)
]
.spacing(8),
)
.width(Fill)
.height(Fill)
.padding(12)
.style(style::card),
)
.width(Fill)
.height(Fill)
+164 -21
View File
@@ -34,9 +34,15 @@ mod view_options_tests;
#[path = "diagram/port_labels.rs"]
mod port_labels;
pub use port_labels::PortLabels;
#[path = "diagram/focus.rs"]
pub(crate) mod focus;
#[path = "diagram/work_drag.rs"]
mod work_drag;
pub use work_drag::WorkFeedback;
#[derive(Clone, Copy)]
pub struct DiagramCanvas<'a> {
pub allocation: Option<crate::app::allocation_drag::Capability<'a>>,
pub project: &'a Project,
pub view_index: Option<&'a oa_core::view_options::ViewIndex>,
pub diagram: Id,
@@ -51,8 +57,9 @@ pub struct DiagramCanvas<'a> {
}
#[derive(Default)]
pub struct State {
work: Option<Arc<crate::app::allocation_drag::Press>>,
work_cache: RefCell<Option<Arc<Scene>>>,
pointer: Option<Point>,
keyboard_focus: bool,
drag: Option<(Id, Point, Position)>,
preview: Option<Position>,
pan_start: Option<(mouse::Button, Point)>,
@@ -64,6 +71,7 @@ pub struct State {
cache: RefCell<Option<CachedScene>>,
}
struct Scene {
work: Option<Arc<crate::app::allocation_drag::Preview>>,
view: ProjectedView,
routed: Arc<RoutedView>,
}
@@ -450,6 +458,26 @@ impl DiagramCanvas<'_> {
}
}
fn scene(&self, state: &State) -> Arc<Scene> {
if let Some(preview) = self.work_preview()
&& state
.work
.as_ref()
.is_some_and(|p| p.token == preview.token)
{
if let Some(scene) = state.work_cache.borrow().as_ref()
&& scene.work.as_ref().is_some_and(|p| Arc::ptr_eq(p, preview))
{
return scene.clone();
}
let scene = Arc::new(Scene {
work: Some(preview.clone()),
view: preview.placement.view.clone(),
routed: preview.routed.clone(),
});
*state.work_cache.borrow_mut() = Some(scene.clone());
return scene;
}
*state.work_cache.borrow_mut() = None;
let preview = if state.context == Some(self.context) {
state.preview
} else {
@@ -496,7 +524,11 @@ impl DiagramCanvas<'_> {
route_diagram(self.project, &diagram, &view)
},
);
let scene = Arc::new(Scene { view, routed });
let scene = Arc::new(Scene {
work: None,
view,
routed,
});
*state.cache.borrow_mut() = Some(CachedScene {
context: self.context,
preview,
@@ -527,7 +559,26 @@ impl canvas::Program<Message> for DiagramCanvas<'_> {
bounds: Rectangle,
cursor: mouse::Cursor,
) -> Option<Action<Message>> {
// A completed work click retires App's allocation epoch before the
// retained canvas sees a redraw. Its already-idle press must not eat
// the first Delete. A live or not-yet-dispatched press stays guarded.
if matches!(event, canvas::Event::Keyboard(iced::keyboard::Event::KeyPressed { key, .. })
if *key == iced::keyboard::Key::Named(iced::keyboard::key::Named::Delete))
&& self.allocation.is_some_and(|capability| {
capability.clean
&& state
.work
.as_ref()
.is_some_and(|press| press.stamp.epoch != capability.state.epoch)
})
{
state.work = None;
}
if let Some(action) = self.work_lifetime(state, event, bounds) {
return Some(action);
}
if state.context != Some(self.context) {
state.work = None;
state.pointer = None;
state.port_drag = None;
state.connection_press = None;
@@ -593,9 +644,6 @@ impl canvas::Program<Message> for DiagramCanvas<'_> {
let local = cursor
.position()
.map(|p| Point::new(p.x - bounds.x, p.y - bounds.y));
if matches!(event, canvas::Event::Mouse(mouse::Event::ButtonPressed(_))) {
state.keyboard_focus = cursor.is_over(bounds);
}
match event {
canvas::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right))
if cursor.is_over(bounds) =>
@@ -615,7 +663,8 @@ impl canvas::Program<Message> for DiagramCanvas<'_> {
}
canvas::Event::Keyboard(iced::keyboard::Event::KeyPressed {
key, modifiers, ..
}) if state.keyboard_focus => {
}) => {
// The mounted focus::Owner is the sole keyboard admission gate.
use iced::keyboard::{Key, key::Named};
match key.as_ref() {
Key::Named(Named::F2)
@@ -670,10 +719,37 @@ impl canvas::Program<Message> for DiagramCanvas<'_> {
Key::Named(Named::Delete)
if self.editable
&& self.connection.is_none()
&& modifiers.is_empty()
&& self.selected_bend.is_some() =>
&& !self.placing
&& state.drag.is_none()
&& state.route_drag.is_none()
&& state.port_drag.is_none()
&& state.pan_start.is_none()
&& state.work.is_none()
&& (modifiers.is_empty()
|| *modifiers == iced::keyboard::Modifiers::SHIFT) =>
{
Some(Action::publish(Message::Route(RouteEvent::RemoveBend)).and_capture())
// An invalid/stale bend never falls through to whole-item removal.
if self.selected_bend.is_some() {
return modifiers.is_empty().then(|| {
Action::publish(Message::Route(RouteEvent::RemoveBend))
.and_capture()
});
}
let request = self.selection_context_request(state, bounds);
if request.object == self.diagram {
return None;
}
Some(
Action::publish(Message::DiagramRemove(
crate::app::diagram_removal::Intent {
context: self.context,
object: request.object,
placement: request.placement,
model: modifiers.shift(),
},
))
.and_capture(),
)
}
_ => None,
}
@@ -714,6 +790,22 @@ impl canvas::Program<Message> for DiagramCanvas<'_> {
let scene = self.scene(state);
let view = &scene.view;
let world = self.world(p);
if self.work_dirty_press(view, &scene.routed, p) {
state.work = None;
state.drag = None;
state.preview = None;
state.port_drag = None;
state.route_drag = None;
return Some(
Action::publish(Message::WorkDrag(
crate::app::allocation_drag::Event::Refused(
self.allocation.unwrap().state.epoch,
crate::app::allocation_drag::FINISH_EDIT.into(),
),
))
.and_capture(),
);
}
if self.placing && self.editable {
return Some(
Action::publish(Message::Architecture(
@@ -795,6 +887,16 @@ impl canvas::Program<Message> for DiagramCanvas<'_> {
return Some(Action::publish(Message::Select(port.element)).and_capture());
}
if let Some(id) = self.hit(view, p) {
if self.work_eligible(id) {
return Some(self.begin_work(
state,
id,
cursor.position()?,
bounds,
view,
&scene.routed,
));
}
let position = self
.project
.diagrams
@@ -833,6 +935,18 @@ impl canvas::Program<Message> for DiagramCanvas<'_> {
}
}
canvas::Event::Mouse(mouse::Event::CursorMoved { .. }) => {
if let Some(press) = &state.work {
return Some(
Action::publish(Message::WorkDrag(
crate::app::allocation_drag::Event::Preview(
press.stamp,
press.token,
cursor.position(),
),
))
.and_capture(),
);
}
let p = local?;
if let Some((button, start)) = state.pan_start {
state.pan_start = Some((button, p));
@@ -865,6 +979,18 @@ impl canvas::Program<Message> for DiagramCanvas<'_> {
None
}
canvas::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
if let Some(press) = state.work.take() {
return Some(
Action::publish(Message::WorkDrag(
crate::app::allocation_drag::Event::Release(
press.stamp,
press.token,
cursor.position(),
),
))
.and_capture(),
);
}
if let Some((button, start)) = state.pan_start {
if button == mouse::Button::Middle {
return Some(Action::capture());
@@ -1011,6 +1137,10 @@ impl canvas::Program<Message> for DiagramCanvas<'_> {
f.translate(self.pan);
f.scale(self.zoom);
let scene = self.scene(state);
let project = scene
.work
.as_ref()
.map_or(self.project, |p| p.semantic.project());
let view = &scene.view;
let containers = view
.nodes
@@ -1019,10 +1149,10 @@ impl canvas::Program<Message> for DiagramCanvas<'_> {
.collect::<BTreeSet<_>>();
for n in &view.nodes {
if containers.contains(&n.id) {
paint_node(&mut f, self.project, n, self.selected, true);
paint_node(&mut f, project, n, self.selected, true);
}
}
let highlighted = highlighted_exchanges(self.project, self.selected);
let highlighted = highlighted_exchanges(project, self.selected);
for edge in &view.edges {
let Some(routed) = scene.routed.edges.get(&edge.id) else {
continue;
@@ -1045,7 +1175,7 @@ impl canvas::Program<Message> for DiagramCanvas<'_> {
} else {
Color::from_rgb8(144, 158, 173)
};
let capability_label = relationship_stereotype(self.project, edge.id);
let capability_label = relationship_stereotype(project, edge.id);
let mut stroke =
Stroke::default()
.with_color(color)
@@ -1093,7 +1223,7 @@ impl canvas::Program<Message> for DiagramCanvas<'_> {
..Default::default()
});
}
if let Some(relation) = self.project.relations.get(&edge.id) {
if let Some(relation) = project.relations.get(&edge.id) {
for end in routing::association_end_geometry(relation, routed) {
if end.navigable {
let [a, tip, b] = navigation_mark(end.tip, end.inward);
@@ -1127,13 +1257,13 @@ impl canvas::Program<Message> for DiagramCanvas<'_> {
}
for n in &view.nodes {
if !containers.contains(&n.id) {
paint_node(&mut f, self.project, n, self.selected, false);
paint_node(&mut f, project, n, self.selected, false);
} else {
// Container headers stay readable when relationships traverse
// the allocation enclosure. Children remain on top.
let mut header = n.clone();
header.size.height = 62.;
paint_node(&mut f, self.project, &header, self.selected, true);
paint_node(&mut f, project, &header, self.selected, true);
}
}
for port in &view.ports {
@@ -1141,17 +1271,17 @@ impl canvas::Program<Message> for DiagramCanvas<'_> {
style::TEAL
} else {
style::kind_color(
self.project.elements[&port.element]
project.elements[&port.element]
.parent
.and_then(|id| self.project.elements.get(&id))
.and_then(|id| project.elements.get(&id))
.map(|e| e.kind())
.unwrap_or(Kind::Port),
)
};
let origin = Point::new(port.position.x - 7., port.position.y - 7.);
let glyph = port_glyph(&self.project.elements[&port.element]);
let glyph = port_glyph(&project.elements[&port.element]);
let path = if glyph == PortGlyph::Function {
let corners = port_triangle(&self.project.elements[&port.element], port);
let corners = port_triangle(&project.elements[&port.element], port);
Path::new(|p| {
p.move_to(corners[0]);
p.line_to(corners[1]);
@@ -1181,7 +1311,7 @@ impl canvas::Program<Message> for DiagramCanvas<'_> {
if matches!(glyph, PortGlyph::Flow(_)) {
f.stroke(
&Path::new(|p| {
for [a, b] in flow_mark(&self.project.elements[&port.element], port) {
for [a, b] in flow_mark(&project.elements[&port.element], port) {
p.move_to(a);
p.line_to(b);
}
@@ -1343,7 +1473,8 @@ impl canvas::Program<Message> for DiagramCanvas<'_> {
) -> mouse::Interaction {
if (self.connection.is_some() || self.placing) && cursor.is_over(bounds) {
mouse::Interaction::Crosshair
} else if state.drag.is_some()
} else if state.work.is_some()
|| state.drag.is_some()
|| state.pan_start.is_some()
|| state.route_drag.is_some()
|| state.port_drag.is_some()
@@ -1900,6 +2031,7 @@ mod tests {
editable: true,
placing: false,
connection: None,
allocation: None,
diagram,
selected: p.id,
zoom: 1.,
@@ -1974,6 +2106,7 @@ mod tests {
editable: true,
placing: false,
connection: None,
allocation: None,
diagram,
selected: p.id,
zoom: 0.6,
@@ -2098,6 +2231,7 @@ mod tests {
editable: true,
placing: false,
connection: Some(&session),
allocation: None,
};
let bounds = Rectangle::new(Point::new(260., 300.), Size::new(2400., 1600.));
let at = |port: &oa_core::projection::ProjectedPort| {
@@ -2171,6 +2305,7 @@ mod tests {
armed.start(&p, &view, source.element);
let armed_canvas = DiagramCanvas {
connection: Some(&armed),
allocation: None,
..canvas
};
// Winit updates cursor coordinates through CursorMoved, not the
@@ -2200,6 +2335,7 @@ mod tests {
// A tool cancellation or navigation invalidates a queued release.
let cancelled = DiagramCanvas {
connection: None,
allocation: None,
context: Context {
epoch: 1,
..context
@@ -2271,6 +2407,7 @@ mod tests {
editable: true,
placing: false,
connection: None,
allocation: None,
};
let mut state = State::default();
let pin = Point::new(400. * zoom + 150., -100. * zoom + 300.);
@@ -2346,6 +2483,7 @@ mod tests {
editable: true,
placing: false,
connection: None,
allocation: None,
};
let mut state = State::default();
let pin = Point::new(350., 250.);
@@ -2442,6 +2580,7 @@ mod tests {
editable: true,
placing: false,
connection: None,
allocation: None,
};
let mut state = State::default();
let first = canvas.scene(&state);
@@ -2569,6 +2708,7 @@ mod tests {
editable: true,
placing: false,
connection: None,
allocation: None,
};
for end in ends {
assert!(svg.contains(&format!(
@@ -2622,6 +2762,7 @@ mod tests {
editable: true,
placing: false,
connection: None,
allocation: None,
};
let mut state = State::default();
let scene = canvas.scene(&state);
@@ -2695,6 +2836,7 @@ mod tests {
editable: true,
placing: false,
connection: None,
allocation: None,
};
assert!(
matches!(pointer(&canvas,&mut state,mouse::Event::ButtonPressed(mouse::Button::Left),point(cover)+canvas.pan),Some(Message::SelectNode(id)) if id==node.id)
@@ -2727,6 +2869,7 @@ mod tests {
editable: true,
placing: false,
connection: None,
allocation: None,
};
let span = &routed.edges[&edge].points;
let pick = point(oa_core::routing::point_along_polyline(span, 0.4).unwrap());
+244
View File
@@ -0,0 +1,244 @@
//! Iced focus ownership shared by architecture and persisted state canvases.
//! Focus operations (including clean Inspector focus) unfocus this widget.
use crate::route_edit::Context;
use iced::advanced::widget::{Operation, Tree, operation};
use iced::advanced::{Clipboard, Layout, Shell, Widget, layout, overlay, renderer};
use iced::{Element, Event as InputEvent, Length, Point, Rectangle, Size, Theme, Vector, mouse};
#[derive(Default)]
struct Input {
context: Option<Context>,
focused: bool,
traversing: bool,
pointer: Option<Point>,
}
impl operation::Focusable for Input {
fn is_focused(&self) -> bool {
self.focused
}
fn focus(&mut self) {
self.focused = true;
self.traversing = false;
}
fn unfocus(&mut self) {
self.focused = false;
self.traversing = false;
}
}
struct Owner<'a, Message, R: renderer::Renderer> {
context: Context,
enabled: bool,
traverse: Box<dyn Fn(bool) -> Message + 'a>,
content: Element<'a, Message, Theme, R>,
}
pub fn wrap<'a, Message: 'a, R: renderer::Renderer + 'a>(
content: impl Into<Element<'a, Message, Theme, R>>,
context: Context,
enabled: bool,
traverse: impl Fn(bool) -> Message + 'a,
) -> Element<'a, Message, Theme, R> {
Element::new(Owner {
context,
enabled,
traverse: Box::new(traverse),
content: content.into(),
})
}
impl<Message, R: renderer::Renderer> Widget<Message, Theme, R> for Owner<'_, Message, R> {
fn tag(&self) -> iced::advanced::widget::tree::Tag {
iced::advanced::widget::tree::Tag::of::<Input>()
}
fn state(&self) -> iced::advanced::widget::tree::State {
iced::advanced::widget::tree::State::new(Input {
context: Some(self.context),
..Default::default()
})
}
fn children(&self) -> Vec<Tree> {
vec![Tree::new(self.content.as_widget())]
}
fn diff(&self, tree: &mut Tree) {
let state = tree.state.downcast_mut::<Input>();
if state.context != Some(self.context) || !self.enabled {
state.focused = false;
state.traversing = false;
state.pointer = None;
}
state.context = Some(self.context);
tree.diff_children(std::slice::from_ref(&self.content));
}
fn size(&self) -> Size<Length> {
self.content.as_widget().size()
}
fn layout(&mut self, tree: &mut Tree, renderer: &R, limits: &layout::Limits) -> layout::Node {
self.content
.as_widget_mut()
.layout(&mut tree.children[0], renderer, limits)
}
fn operate(
&mut self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &R,
operation: &mut dyn Operation,
) {
if self.enabled {
operation.focusable(
Some(&iced::advanced::widget::Id::from("diagram-keyboard-owner")),
layout.bounds(),
tree.state.downcast_mut::<Input>(),
);
}
self.content
.as_widget_mut()
.operate(&mut tree.children[0], layout, renderer, operation);
}
fn update(
&mut self,
tree: &mut Tree,
event: &InputEvent,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &R,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) {
let state = tree.state.downcast_mut::<Input>();
if let InputEvent::Mouse(mouse::Event::CursorMoved { position }) = event {
state.pointer = Some(*position);
}
if matches!(
event,
InputEvent::Window(iced::window::Event::Unfocused | iced::window::Event::Resized(_))
) || !self.enabled
{
state.focused = false;
state.traversing = false;
state.pointer = None;
}
if matches!(event, InputEvent::Mouse(mouse::Event::ButtonPressed(_))) {
state.traversing = false;
let pointer = state.pointer.or_else(|| cursor.position());
state.focused = self.enabled
&& !shell.is_event_captured()
&& cursor.position().is_some()
&& pointer.is_some_and(|p| {
layout
.bounds()
.intersection(viewport)
.is_some_and(|b| b.contains(p))
});
}
if let InputEvent::Keyboard(iced::keyboard::Event::KeyPressed {
key: iced::keyboard::Key::Named(iced::keyboard::key::Named::Tab),
modifiers,
..
}) = event
&& state.focused
&& !state.traversing
&& self.enabled
&& !shell.is_event_captured()
&& (*modifiers == iced::keyboard::Modifiers::SHIFT || modifiers.is_empty())
{
// The focus task runs after the native batch. Retain focus for its
// traversal count, but reject later keys in this same batch now.
state.traversing = true;
shell.publish((self.traverse)(modifiers.shift()));
shell.capture_event();
return;
}
if matches!(event, InputEvent::Keyboard(_))
&& (!state.focused || state.traversing || !self.enabled)
{
return;
}
if shell.is_event_captured() {
return;
}
self.content.as_widget_mut().update(
&mut tree.children[0],
event,
layout,
cursor,
renderer,
clipboard,
shell,
viewport,
);
}
fn draw(
&self,
tree: &Tree,
renderer: &mut R,
theme: &Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
) {
self.content.as_widget().draw(
&tree.children[0],
renderer,
theme,
style,
layout,
cursor,
viewport,
);
let state = tree.state.downcast_ref::<Input>();
if state.focused && !state.traversing && self.enabled {
renderer.fill_quad(
renderer::Quad {
bounds: layout.bounds(),
border: iced::Border {
color: crate::style::TEAL,
width: 1.,
..Default::default()
},
..Default::default()
},
iced::Color::TRANSPARENT,
);
}
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &R,
) -> mouse::Interaction {
self.content.as_widget().mouse_interaction(
&tree.children[0],
layout,
cursor,
viewport,
renderer,
)
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'b>,
renderer: &R,
viewport: &Rectangle,
translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, R>> {
self.content.as_widget_mut().overlay(
&mut tree.children[0],
layout,
renderer,
viewport,
translation,
)
}
}
@@ -64,6 +64,7 @@ fn canvas(app: &App) -> DiagramCanvas<'_> {
editable: true,
placing: false,
connection: app.connection.as_ref(),
allocation: None,
}
}
fn screen(app: &App, id: Id) -> Point {
@@ -72,6 +72,7 @@ fn canvas(app: &App, zoom: f32) -> DiagramCanvas<'_> {
editable: true,
placing: false,
connection: app.connection.as_ref(),
allocation: None,
}
}
fn event(
@@ -18,6 +18,7 @@ fn canvas(project: &Project, diagram: Id, zoom: f32) -> DiagramCanvas<'_> {
editable: true,
placing: false,
connection: None,
allocation: None,
}
}
@@ -58,6 +58,9 @@ impl canvas::Program<Message> for PortLabels<'_> {
bounds: Rectangle,
cursor: mouse::Cursor,
) -> Vec<canvas::Geometry> {
if self.0.work_preview().is_some() {
return vec![]; // The candidate canvas already paints its own ports.
}
if state.context == Some(self.0.context) && state.held_buttons != 0 {
return vec![];
}
@@ -185,6 +188,7 @@ mod tests {
editable: true,
placing: false,
connection: None,
allocation: None,
})
}
+205
View File
@@ -0,0 +1,205 @@
use super::*;
use crate::app::allocation_drag::{self as input, Event, Press, Stamp};
impl DiagramCanvas<'_> {
pub(crate) fn work_dirty_point(&self, bounds: Rectangle, screen: Point) -> bool {
let diagram = &self.project.diagrams[&self.diagram];
let view = self.projected(diagram);
let routed = routing::route_diagram(self.project, diagram, &view);
self.work_dirty_press(
&view,
&routed,
Point::new(screen.x - bounds.x, screen.y - bounds.y),
)
}
pub(super) fn work_preview(&self) -> Option<&Arc<input::Preview>> {
let capability = self.allocation?;
let feedback = capability.state.feedback.as_ref()?;
(capability.clean
&& feedback.press.stamp.context == self.context
&& feedback.press.stamp.epoch == capability.state.epoch
&& feedback.press.stamp.pan == self.pan
&& feedback.press.stamp.zoom == self.zoom)
.then_some(capability.state.preview.as_ref())
.flatten()
}
pub(super) fn work_eligible(&self, placement: Id) -> bool {
self.allocation.is_some()
&& self.project.diagrams[&self.diagram].projection == ProjectionKind::Allocation
&& self.project.diagrams[&self.diagram]
.nodes
.get(&placement)
.and_then(|n| self.project.elements.get(&n.element))
.is_some_and(|e| e.kind().is_work())
}
pub(super) fn work_dirty_press(
&self,
view: &ProjectedView,
routed: &RoutedView,
local: Point,
) -> bool {
self.allocation.is_some_and(|c| !c.clean)
&& !self.placing
&& self.connection.is_none()
&& self.project.diagrams[&self.diagram].projection == ProjectionKind::Allocation
&& (self.hit(view, local).is_some()
|| self.hit_port(view, local).is_some()
|| self.hit_edge(routed, local).is_some()
|| self.hit_label(routed, local).is_some()
|| self.hit_association_label(routed, local).is_some())
}
pub(super) fn begin_work(
&self,
state: &mut State,
node: Id,
screen: Point,
bounds: Rectangle,
view: &ProjectedView,
routed: &RoutedView,
) -> Action<Message> {
let capability = self.allocation.unwrap();
state.drag = None;
state.preview = None;
state.port_drag = None;
state.route_drag = None;
if !capability.clean {
return Action::publish(Message::WorkDrag(Event::Refused(
capability.state.epoch,
input::FINISH_EDIT.into(),
)))
.and_capture();
}
let work = self.project.diagrams[&self.diagram].nodes[&node].element;
let scope = match crate::allocation_drag::capture(self.project, self.diagram, work, node) {
Ok(scope) => scope,
Err(reason) => {
return Action::publish(Message::WorkDrag(Event::Refused(
capability.state.epoch,
input::refusal(&reason),
)))
.and_capture();
}
};
let frozen = input::Frozen::capture(self.project, &scope, view, routed, self.zoom);
let press = Arc::new(Press {
stamp: Stamp {
context: self.context,
epoch: capability.state.epoch,
snapshot: capability.snapshot,
bounds,
pan: self.pan,
zoom: self.zoom,
},
token: Id::new_v4(),
start: screen,
scope,
frozen,
arrange: capability.state.arrange_next,
});
state.work = Some(press.clone());
Action::publish(Message::WorkDrag(Event::Press(press))).and_capture()
}
pub(super) fn work_lifetime(
&self,
state: &mut State,
event: &canvas::Event,
bounds: Rectangle,
) -> Option<Action<Message>> {
let press = state.work.as_ref()?;
let cancel = self
.allocation
.is_none_or(|c| !c.clean || c.state.epoch != press.stamp.epoch)
|| press.stamp.context != self.context
|| press.stamp.bounds != bounds
|| press.stamp.pan != self.pan
|| press.stamp.zoom != self.zoom
|| matches!(
event,
canvas::Event::Window(iced::window::Event::Unfocused)
| canvas::Event::Mouse(mouse::Event::CursorLeft)
| canvas::Event::Mouse(mouse::Event::ButtonPressed(
mouse::Button::Middle | mouse::Button::Right
))
| canvas::Event::Mouse(mouse::Event::WheelScrolled { .. })
)
|| matches!(event, canvas::Event::Keyboard(iced::keyboard::Event::KeyPressed { key, .. })
if *key == iced::keyboard::Key::Named(iced::keyboard::key::Named::Escape));
if cancel {
let token = press.token;
state.work = None;
state.pointer = None;
return Some(Action::publish(Message::WorkDrag(Event::Cancel(token))).and_capture());
}
None
}
}
/// Separate passive layer: frozen target labels remain above all candidate card
/// text. It does not acquire input or use a second independent semantic scene.
pub struct WorkFeedback<'a>(pub DiagramCanvas<'a>);
impl canvas::Program<Message> for WorkFeedback<'_> {
type State = ();
fn draw(
&self,
_: &(),
renderer: &Renderer,
_: &Theme,
bounds: Rectangle,
_: mouse::Cursor,
) -> Vec<canvas::Geometry> {
let Some(capability) = self.0.allocation else {
return vec![];
};
let Some(feedback) = &capability.state.feedback else {
return vec![];
};
if feedback.press.stamp.epoch != capability.state.epoch
|| feedback.press.stamp.context != self.0.context
|| feedback.press.stamp.bounds != bounds
{
return vec![];
}
let mut frame = Frame::new(renderer, bounds.size());
let color = if feedback.refused {
Color::from_rgb8(173, 66, 41)
} else {
style::TEAL
};
for index in &feedback.targets {
let target = &feedback.press.frozen.targets[*index];
let region = target.region;
let top = Point::new(
region.x * self.0.zoom + self.0.pan.x,
region.y * self.0.zoom + self.0.pan.y,
);
frame.stroke(
&Path::rectangle(
top,
iced::Size::new(region.width * self.0.zoom, region.height * self.0.zoom),
),
Stroke::default().with_color(color).with_width(3.),
);
let label = target.label;
let top = Point::new(
label.position.x * self.0.zoom + self.0.pan.x,
label.position.y * self.0.zoom + self.0.pan.y,
);
frame.fill_rectangle(
top,
iced::Size::new(
label.size.width * self.0.zoom,
label.size.height * self.0.zoom,
),
Color::WHITE,
);
frame.fill_text(Text {
content: target.name.clone(),
position: top,
size: (16. * self.0.zoom).into(),
color,
..Default::default()
});
}
vec![frame.into_geometry()]
}
}
+18 -4
View File
@@ -95,7 +95,7 @@ pub struct Document {
past: Vec<HistoryEntry>,
future: Vec<HistoryEntry>,
current_bytes: usize,
saved: Arc<SourceBundle>,
saved: Option<Arc<SourceBundle>>,
revision: u64,
view_index: std::cell::OnceCell<oa_core::view_options::ViewIndex>,
limits: ProjectLimits,
@@ -125,7 +125,7 @@ impl Document {
current_bytes,
past: Vec::new(),
future: Vec::new(),
saved: Arc::new(bundle),
saved: Some(Arc::new(bundle)),
revision: 0,
view_index: Default::default(),
limits,
@@ -156,7 +156,7 @@ impl Document {
}
pub fn dirty(&self) -> bool {
self.recovered_unsaved || self.bundle() != self.saved.as_ref()
self.recovered_unsaved || self.saved.as_deref() != Some(self.bundle())
}
/// Recovered work is a detached copy until explicitly saved by its user.
@@ -181,10 +181,24 @@ impl Document {
/// Records the bytes actually saved, even when later edits remain current.
/// The I/O adapter owns document identity and save completion ordering.
pub fn mark_saved(&mut self, snapshot: Arc<CompiledSourceProject>) {
self.saved = Arc::new(snapshot.bundle.clone());
self.saved = Some(Arc::new(snapshot.bundle.clone()));
self.recovered_unsaved = false;
}
/// Bind an installed or recovered repair destination to source read there.
/// These bytes may be invalid or differ from the retained accepted model,
/// but must replace the previous disk baseline. Accepted history is retained;
/// applying or undoing to old saved content does not save this destination.
pub(crate) fn bind_repair_baseline(&mut self, source: SourceBundle) {
self.saved = Some(Arc::new(source));
}
/// Recovery changed the current destination, but its source could not be
/// reloaded. No accepted or historical snapshot is known to be saved there.
pub(crate) fn invalidate_saved_baseline(&mut self) {
self.saved = None;
}
/// Check a source Save as before writing any files. A retained included file
/// must never be displaced by the new entry, including on a later Undo.
pub fn check_save_entry(&self, entry: &str) -> Result<(), DocumentError> {
+22 -37
View File
@@ -18,8 +18,7 @@ mod context_fields;
mod data_fields;
mod history_fields;
mod port_fields;
#[cfg(test)]
mod reference_chooser;
pub(crate) mod reference_chooser;
mod view_fields;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -87,6 +86,8 @@ pub struct Draft {
pub value_browsers: BTreeMap<String, ValueBrowser>,
pub reference_browsers: BTreeMap<String, ReferenceBrowser>,
history: history_fields::State,
reference_session: Id,
reference_generation: Id,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
@@ -211,11 +212,14 @@ impl Draft {
value_browsers: BTreeMap::new(),
reference_browsers: BTreeMap::new(),
history: Default::default(),
reference_session: Id::new_v4(),
reference_generation: Id::new_v4(),
};
draft.rebuild();
Some(draft)
}
fn rebuild(&mut self) {
self.reference_generation = Id::new_v4();
// Array/variant edits can change the meaning of a row path.
self.value_browsers.clear();
self.inputs.clear();
@@ -276,6 +280,9 @@ impl Draft {
.unwrap_or_else(|| self.materialize().unwrap_or_else(|_| self.value.clone()))
}
pub fn set_context(&mut self, context: Option<Value>) {
if self.context != context {
self.reference_generation = Id::new_v4();
}
self.context = context;
}
/// Materialize selected scalar subtrees without parsing retained, unselected drafts.
@@ -361,6 +368,9 @@ impl Draft {
}
pub fn input(&mut self, path: &str, value: String) {
if let Some(input) = self.inputs.get_mut(path) {
if input.raw != value {
self.reference_generation = Id::new_v4();
}
input.editor = text_editor::Content::with_text(&value);
input.raw = value;
}
@@ -368,7 +378,11 @@ impl Draft {
pub fn edit_text(&mut self, path: &str, action: text_editor::Action) {
if let Some(input) = self.inputs.get_mut(path) {
input.editor.perform(action);
input.raw = input.editor.text();
let raw = input.editor.text();
if input.raw != raw {
self.reference_generation = Id::new_v4();
}
input.raw = raw;
}
}
pub fn shape(&mut self, path: &str, value: Value) -> Result<(), String> {
@@ -1797,7 +1811,7 @@ pub fn view<'a>(draft: &'a Draft, project: &'a Project) -> View<'a, Message> {
));
}
}
content.into()
reference_chooser::trigger::scope(content, draft.reference_session)
}
/// Render one closed property subtree for a workflow-owned typed draft.
@@ -1939,41 +1953,12 @@ fn property<'a>(
return out.into();
}
if reference(path) && (value.is_string() || value.is_null()) {
let raw = draft
.inputs
.get(path)
.map(|v| v.raw.as_str())
.or_else(|| value.as_str());
let current = raw.and_then(|s| s.parse::<Id>().ok()).unwrap_or(Id::nil());
let choices = reference_choices(draft, project, path, current);
let selected = choices.iter().find(|c| c.id == current).cloned();
let owned_path = path.to_string();
let optional = optional_reference(path);
let mut content = column![
return column![
label_view(),
pick_list(choices, selected, move |choice| Message::FieldShape(
owned_path.clone(),
if choice.id.is_nil() && optional {
Value::Null
} else {
json!(choice.id)
}
))
.text_size(12)
.padding(8)
.width(Fill)
reference_chooser::adapter::field(draft, project, path)
]
.spacing(5);
if let Some(raw) = raw
&& raw.parse::<Id>().is_err()
{
content = content.push(
text(format!("Invalid reference retained in the draft: {raw}"))
.size(11)
.color(style::MUTED),
);
}
return content.into();
.spacing(5)
.into();
}
match value {
Value::Object(map) => {
+1 -1
View File
@@ -670,7 +670,7 @@ mod tests {
}
fn semantic_send(app: &mut crate::app::App, event: crate::semantic_review::RowEvent) {
let event = semantic_state(app).address_event(event);
let _ = app.update(Message::Workflow(crate::workflow::Event::Semantic(event)));
let _ = app.update(Message::Workflow(crate::workflow::Event::semantic(event)));
}
fn semantic_decide(app: &mut crate::app::App, facet: FieldFacet, decision: SemanticDecision) {
use crate::semantic_review::RowEvent as Event;
@@ -5,8 +5,10 @@
use oa_core::Id;
use std::collections::BTreeSet;
pub mod adapter;
#[cfg(test)]
mod tests;
pub mod trigger;
pub mod widget;
pub const MAX_ROWS: usize = 6;
@@ -0,0 +1,307 @@
//! Live form adapter. Compatibility stays in the existing parent adapter;
//! browser metadata and scoped writes never reinterpret historical UUID text.
use super::super::{Draft, friendly, history_fields, optional_reference, reference, reference_ids};
use super::{Candidate, Context, Current, State, trigger};
use crate::{app::Message, style};
use iced::{
Element, Fill, Rectangle,
widget::{column, container, row, text, tooltip},
};
use oa_core::{Id, Project};
use serde_json::{Value, json};
use std::collections::BTreeSet;
#[derive(Debug, Clone)]
pub struct Open {
pub document: Id,
pub session: Id,
pub generation: Id,
pub record: Id,
pub path: String,
pub anchor: Rectangle,
}
#[derive(Debug, Clone)]
pub struct Set {
pub context: Context,
pub generation: Id,
pub value: Option<Id>,
}
impl Draft {
pub fn reference_session(&self) -> Id {
self.reference_session
}
pub fn reference_generation(&self) -> Id {
self.reference_generation
}
pub fn reference_open(&self, project: &Project, path: &str, anchor: Rectangle) -> Open {
Open {
document: project.id,
session: self.reference_session,
generation: self.reference_generation,
record: self.original,
path: path.into(),
anchor,
}
}
pub fn reference_matches(&self, request: &Open) -> bool {
self.reference_session == request.session
&& self.reference_generation == request.generation
&& self.original == request.record
&& self.live_reference(&request.path)
}
fn live_reference(&self, path: &str) -> bool {
!history_fields::is_path(path)
&& reference(path)
&& self
.value
.pointer(path)
.is_some_and(|v| v.is_string() || v.is_null())
}
pub fn reference_catalog(
&self,
project: &Project,
request: &Open,
revision: u64,
) -> Result<State, String> {
if request.document != project.id || !self.reference_matches(request) {
return Err("The reference field changed. Open its chooser again.".into());
}
let allowed: BTreeSet<_> = reference_ids(self, project, &request.path)
.into_iter()
.filter(|id| !id.is_nil())
.collect();
let candidates = allowed
.iter()
.filter_map(|id| candidate(project, *id))
.collect();
let raw = self
.inputs
.get(&request.path)
.map(|v| v.raw.as_str())
.or_else(|| self.value.pointer(&request.path).and_then(Value::as_str));
let current = match raw {
None => Current::Unassigned,
Some(raw) => match raw.parse::<Id>() {
Ok(id) if id.is_nil() => Current::Unassigned,
Ok(id) => match candidate(project, id) {
Some(c) if allowed.contains(&id) => Current::Valid(c),
Some(c) => Current::Incompatible(c),
None => Current::Missing(id),
},
Err(_) => Current::InvalidRaw(raw.into()),
},
};
let context = Context::live(
project.id,
revision,
self.reference_session,
self.original,
&request.path,
optional_reference(&request.path),
)
.map_err(str::to_owned)?;
State::open(
context,
candidates,
current,
format!(
"{} · compatible references",
friendly(request.path.rsplit('/').next().unwrap_or("Reference"))
),
)
.map_err(str::to_owned)
}
pub fn reference_set(&mut self, project: &Project, intent: &Set) -> Result<(), String> {
let value = self.reference_value(project, intent)?;
self.shape_scoped(&intent.context.path, value)
}
pub fn reference_value(&self, project: &Project, intent: &Set) -> Result<Value, String> {
let context = &intent.context;
if context.document != project.id
|| context.draft != self.reference_session
|| context.record != self.original
|| intent.generation != self.reference_generation
|| !self.live_reference(&context.path)
|| context.optional != optional_reference(&context.path)
{
return Err("The originating reference field is no longer current.".into());
}
let value = match intent.value {
Some(id)
if !id.is_nil() && reference_ids(self, project, &context.path).contains(&id) =>
{
json!(id)
}
None if context.optional => Value::Null,
_ => return Err("Choose a currently compatible reference.".into()),
};
Ok(value)
}
}
fn named(project: &Project, id: Id) -> String {
if let Some(e) = project.elements.get(&id) {
format!("{} · @{}", e.name, e.alias)
} else {
project.named(id).into()
}
}
fn ancestry(project: &Project, id: Id) -> String {
let mut parts = vec![];
let mut seen = BTreeSet::new();
let mut current = project.elements.get(&id).and_then(|e| e.parent);
while let Some(id) = current {
if !seen.insert(id) {
parts.push("[containment cycle]".into());
break;
}
parts.push(named(project, id));
current = project.elements.get(&id).and_then(|e| e.parent);
}
parts.reverse();
if parts.is_empty() {
project.name.clone()
} else {
parts.join(" / ")
}
}
pub(crate) fn candidate(project: &Project, id: Id) -> Option<Candidate> {
let layer = |layer| format!("{layer:?}").to_uppercase();
if id == project.id {
return Some(Candidate {
id,
name: project.name.clone(),
alias: project.alias.clone(),
kind: "Project".into(),
scope: "Model".into(),
containment: "Model root".into(),
});
}
if let Some(e) = project.elements.get(&id) {
return Some(Candidate {
id,
name: e.name.clone(),
alias: e.alias.clone(),
kind: e.kind().to_string(),
scope: layer(e.layer()),
containment: ancestry(project, id),
});
}
if let Some(r) = project.relations.get(&id) {
return Some(Candidate {
id,
name: r.name.clone(),
alias: r.alias.clone(),
kind: r.kind().to_string(),
scope: project.layer_of(id).map_or_else(|| "Model".into(), layer),
containment: format!(
"{} → {}",
named(project, r.source()),
named(project, r.target())
),
});
}
for diagram in project.diagrams.values() {
if id == diagram.id {
return Some(Candidate {
id,
name: diagram.name.clone(),
alias: diagram.alias.clone(),
kind: format!("{:?} diagram", diagram.projection),
scope: layer(diagram.layer),
containment: project.name.clone(),
});
}
if let Some(node) = diagram.nodes.get(&id) {
return Some(Candidate {
id,
name: project.named(node.element).into(),
alias: node.alias.clone(),
kind: "Placement".into(),
scope: layer(diagram.layer),
containment: format!(
"{} · @{} / {}",
diagram.name,
diagram.alias,
named(project, node.element)
),
});
}
}
if let Some(link) = project.artifact_links.get(&id) {
return Some(Candidate {
id,
name: link.name.clone(),
alias: link.alias.clone(),
kind: "Artifact link".into(),
scope: project
.layer_of(link.owner)
.map_or_else(|| "Model".into(), layer),
containment: named(project, link.owner),
});
}
project.traces.get(&id).map(|trace| Candidate {
id,
name: trace.name.clone(),
alias: trace.alias.clone(),
kind: "Trace".into(),
scope: "OA ↔ SA".into(),
containment: format!(
"{} ↔ {}",
named(project, trace.oa),
named(project, trace.sa)
),
})
}
pub fn field<'a, R: iced::advanced::text::Renderer<Font = iced::Font> + 'a>(
draft: &'a Draft,
project: &'a Project,
path: &str,
) -> Element<'a, Message, iced::Theme, R> {
let raw = draft
.inputs
.get(path)
.map(|v| v.raw.as_str())
.or_else(|| draft.value.pointer(path).and_then(Value::as_str));
let (label, detail) = match raw {
None => (
if optional_reference(path) {
"Not assigned"
} else {
"Choose a reference…"
}
.into(),
String::new(),
),
Some(raw) => match raw.parse::<Id>() {
Ok(id) if id.is_nil() => ("Choose a reference…".into(), String::new()),
Ok(id) => match candidate(project, id) {
Some(c) => (format!("{} · @{}", c.name, c.alias), c.detail()),
None => ("Missing reference".into(), raw.into()),
},
Err(_) => ("Invalid reference retained".into(), raw.into()),
},
};
let label = crate::views::short(&label, 35);
let body = container(row![text(label).size(12).width(Fill), text("⌄").size(12)].spacing(6))
.padding(8)
.width(Fill);
let path = path.to_owned();
let id = trigger::field_id(draft.reference_generation, &path);
let field = trigger::Field::new(body, id, move |anchor| {
Message::FieldReferenceOpen(draft.reference_open(project, &path, anchor))
});
if detail.is_empty() {
field.into()
} else {
tooltip(
field,
container(column![text(detail).size(11)])
.padding(8)
.style(style::card),
tooltip::Position::Left,
)
.into()
}
}
@@ -0,0 +1,447 @@
//! Keyboard entry and focus operations scoped to one property form. Focus
//! targets include the draft generation so delayed operations cannot focus a
//! replacement array row or a recovered document with the same semantic IDs.
use crate::style;
use iced::{
Border, Color, Element, Event, Length, Rectangle, Size, Task, Theme, Vector,
advanced::{
Clipboard, Layout, Shell, Widget, layout, overlay, renderer,
widget::{Id, Operation, Tree, operation, tree},
},
keyboard, mouse,
widget::container,
};
use oa_core::Id as ModelId;
mod focus;
#[derive(Clone, Copy)]
pub struct FormScope {
pub session: ModelId,
pub modal: bool,
}
pub(super) struct FormOwner(pub Option<FormScope>);
const REVIEW_MODAL: &str = "reference-owning-review-modal";
/// Marks the entire Review dialog without changing its layout or event flow.
pub fn modal<'a, Message: 'a, R: renderer::Renderer + 'a>(
content: impl Into<Element<'a, Message, Theme, R>>,
) -> Element<'a, Message, Theme, R> {
Field {
content: content.into(),
id: Some(Id::new(REVIEW_MODAL)),
on_open: None,
}
.into()
}
pub fn field_id(generation: ModelId, path: &str) -> Id {
Id::from(format!("reference:{generation}:{path}"))
}
pub fn scope_id(session: ModelId) -> Id {
Id::from(format!("reference-form:{session}"))
}
#[derive(Default)]
struct Interaction {
focused: bool,
pressed: bool,
identity: Option<Id>,
anchor: Option<Rectangle>,
}
impl operation::Focusable for Interaction {
fn is_focused(&self) -> bool {
self.focused
}
fn focus(&mut self) {
self.focused = true;
}
fn unfocus(&mut self) {
self.focused = false;
self.pressed = false;
}
}
pub struct Field<'a, Message, R = iced::Renderer>
where
R: renderer::Renderer,
{
content: Element<'a, Message, Theme, R>,
id: Option<Id>,
on_open: Option<Box<dyn Fn(Rectangle) -> Message + 'a>>,
}
impl<'a, Message, R: renderer::Renderer> Field<'a, Message, R> {
pub fn new(
content: impl Into<Element<'a, Message, Theme, R>>,
id: Id,
on_open: impl Fn(Rectangle) -> Message + 'a,
) -> Self {
Self {
content: content.into(),
id: Some(id),
on_open: Some(Box::new(on_open)),
}
}
}
pub fn scope<'a, Message: 'a, R: renderer::Renderer + 'a>(
content: impl Into<Element<'a, Message, Theme, R>>,
session: ModelId,
) -> Element<'a, Message, Theme, R> {
container(Field {
content: content.into(),
id: None,
on_open: None,
})
.id(scope_id(session))
.width(Length::Fill)
.into()
}
fn run<Message, R: renderer::Renderer>(
content: &mut Element<'_, Message, Theme, R>,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &R,
mut operation: Box<dyn Operation>,
) -> bool {
let mut changed = false;
loop {
content
.as_widget_mut()
.operate(tree, layout, renderer, &mut *operation);
match operation.finish() {
operation::Outcome::Chain(next) => {
operation = next;
changed = true;
}
_ => break,
}
}
changed
}
pub(super) fn tab<Message, R: renderer::Renderer>(
content: &mut Element<'_, Message, Theme, R>,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &R,
scope: FormScope,
backwards: bool,
) -> bool {
run(
content,
tree,
layout,
renderer,
Box::new(focus::relative(scope.session, backwards, scope.modal)),
)
}
pub(super) fn own_input<Message, R: renderer::Renderer>(
content: &mut Element<'_, Message, Theme, R>,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &R,
) {
content
.as_widget_mut()
.operate(tree, layout, renderer, &mut focus::exclusive());
}
impl<Message, R: renderer::Renderer> Widget<Message, Theme, R> for Field<'_, Message, R> {
fn tag(&self) -> tree::Tag {
tree::Tag::of::<Interaction>()
}
fn state(&self) -> tree::State {
tree::State::new(Interaction {
identity: self.id.clone(),
..Default::default()
})
}
fn children(&self) -> Vec<Tree> {
vec![Tree::new(self.content.as_widget())]
}
fn diff(&self, tree: &mut Tree) {
let state = tree.state.downcast_mut::<Interaction>();
if state.identity != self.id {
*state = Interaction {
identity: self.id.clone(),
..Default::default()
};
}
tree.diff_children(std::slice::from_ref(&self.content));
}
fn size(&self) -> Size<Length> {
self.content.as_widget().size()
}
fn layout(&mut self, tree: &mut Tree, renderer: &R, limits: &layout::Limits) -> layout::Node {
self.content
.as_widget_mut()
.layout(&mut tree.children[0], renderer, limits)
}
fn operate(
&mut self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &R,
operation: &mut dyn Operation,
) {
if let Some(id) = &self.id
&& self.on_open.is_some()
{
operation.focusable(
Some(id),
layout.bounds(),
tree.state.downcast_mut::<Interaction>(),
);
operation.custom(
Some(id),
layout.bounds(),
tree.state.downcast_mut::<Interaction>(),
);
}
if self.on_open.is_none() {
operation.container(self.id.as_ref(), layout.bounds());
operation.traverse(&mut |operation| {
self.content.as_widget_mut().operate(
&mut tree.children[0],
layout,
renderer,
operation,
)
});
} else {
self.content.as_widget_mut().operate(
&mut tree.children[0],
layout,
renderer,
operation,
);
}
}
fn update(
&mut self,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &R,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) {
if let Some(on_open) = &self.on_open {
let state = tree.state.downcast_mut::<Interaction>();
let visible = layout.bounds().intersection(viewport);
let hovered = visible.is_some_and(|bounds| cursor.is_over(bounds));
let activate = match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) if hovered => {
state.focused = true;
state.pressed = true;
shell.capture_event();
shell.request_redraw();
false
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
let activate = state.pressed && hovered;
if state.pressed {
shell.capture_event();
}
state.pressed = false;
activate
}
Event::Keyboard(keyboard::Event::KeyPressed {
key:
keyboard::Key::Named(keyboard::key::Named::Enter | keyboard::key::Named::Space),
repeat: false,
..
}) if state.focused => true,
Event::Mouse(mouse::Event::ButtonPressed(_)) => {
state.focused = false;
state.pressed = false;
false
}
_ => false,
};
if activate && let Some(bounds) = state.anchor {
shell.publish(on_open(bounds));
shell.capture_event();
return;
}
}
self.content.as_widget_mut().update(
&mut tree.children[0],
event,
layout,
cursor,
renderer,
clipboard,
shell,
viewport,
);
}
fn draw(
&self,
tree: &Tree,
renderer: &mut R,
theme: &Theme,
renderer_style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
) {
if self.on_open.is_some() {
let focus = tree.state.downcast_ref::<Interaction>().focused;
renderer.fill_quad(
renderer::Quad {
bounds: layout.bounds(),
border: Border {
color: if focus || cursor.is_over(layout.bounds()) {
style::TEAL
} else {
style::LINE
},
width: if focus { 2. } else { 1. },
radius: 5.into(),
},
..Default::default()
},
Color::WHITE,
);
}
self.content.as_widget().draw(
&tree.children[0],
renderer,
theme,
renderer_style,
layout,
cursor,
viewport,
);
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
renderer: &R,
) -> mouse::Interaction {
if self.on_open.is_some() && cursor.is_over(layout.bounds()) {
mouse::Interaction::Pointer
} else {
self.content.as_widget().mouse_interaction(
&tree.children[0],
layout,
cursor,
viewport,
renderer,
)
}
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'b>,
renderer: &R,
viewport: &Rectangle,
translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, R>> {
self.content.as_widget_mut().overlay(
&mut tree.children[0],
layout,
renderer,
viewport,
translation,
)
}
}
/// Scrollable child updates use content coordinates. The root visits the
/// retained tree before each input, so field anchors use current window
/// coordinates even when a wheel event and activation share one runtime batch.
pub(super) fn measure<Message, R: renderer::Renderer>(
content: &mut Element<'_, Message, Theme, R>,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &R,
viewport: Rectangle,
) {
content.as_widget_mut().operate(
tree,
layout,
renderer,
&mut Measure {
translation: Vector::ZERO,
viewport: Some(viewport),
pending: None,
},
);
}
struct Measure {
translation: Vector,
viewport: Option<Rectangle>,
pending: Option<(Vector, Option<Rectangle>)>,
}
impl Operation for Measure {
fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn Operation)) {
let previous = (self.translation, self.viewport);
if let Some((translation, viewport)) = self.pending.take() {
self.translation = translation;
self.viewport = viewport;
}
visit(self);
(self.translation, self.viewport) = previous;
}
fn scrollable(
&mut self,
_: Option<&Id>,
bounds: Rectangle,
_: Rectangle,
translation: Vector,
_: &mut dyn operation::Scrollable,
) {
let bounds = Rectangle {
x: bounds.x - self.translation.x,
y: bounds.y - self.translation.y,
..bounds
};
self.pending = Some((
self.translation + translation,
self.viewport
.and_then(|viewport| viewport.intersection(&bounds)),
));
}
fn custom(&mut self, _: Option<&Id>, bounds: Rectangle, state: &mut dyn std::any::Any) {
if let Some(state) = state.downcast_mut::<Interaction>() {
let bounds = Rectangle {
x: bounds.x - self.translation.x,
y: bounds.y - self.translation.y,
..bounds
};
state.anchor = self
.viewport
.and_then(|viewport| viewport.intersection(&bounds));
}
}
}
impl<'a, Message: 'a, R: renderer::Renderer + 'a> From<Field<'a, Message, R>>
for Element<'a, Message, Theme, R>
{
fn from(field: Field<'a, Message, R>) -> Self {
Element::new(field)
}
}
pub fn focus_operation(
session: ModelId,
generation: ModelId,
path: &str,
step: i32,
) -> impl Operation + 'static {
focus::exact(session, generation, path, step)
}
pub fn focus<Message: Send + 'static>(
session: ModelId,
generation: ModelId,
path: &str,
step: i32,
) -> Task<Message> {
iced::advanced::widget::operate(focus_operation(session, generation, path, step)).discard()
}
@@ -0,0 +1,313 @@
//! Focus movement and reveal share one root traversal. A form may be inside a
//! scrollable; restricting traversal to its container would hide that ancestor.
use super::{FormOwner, Id, ModelId, Operation, Rectangle, Vector, field_id, operation, scope_id};
use std::collections::BTreeMap;
enum Origin {
Current,
Exact(Id),
}
#[derive(Clone)]
struct Scroll {
index: usize,
bounds: Rectangle,
capacity: Vector,
offset: Vector,
}
struct Field {
id: Option<Id>,
bounds: Rectangle,
ancestors: Vec<Scroll>,
}
pub(super) struct Collect {
scope: Id,
origin: Origin,
step: i32,
inside: bool,
entering: Option<bool>,
translation: Vector,
scrolls: Vec<Scroll>,
pending_scroll: Option<Scroll>,
scroll_index: usize,
fields: Vec<Field>,
focused: Option<usize>,
outside_focused: bool,
modal: bool,
owner_valid: bool,
}
impl Collect {
fn new(scope: Id, origin: Origin, step: i32) -> Self {
let owner_valid = matches!(origin, Origin::Current);
Self {
scope,
origin,
step,
inside: false,
entering: None,
translation: Vector::ZERO,
scrolls: vec![],
pending_scroll: None,
scroll_index: 0,
fields: vec![],
focused: None,
outside_focused: false,
modal: false,
owner_valid,
}
}
}
pub(super) fn relative(session: ModelId, backwards: bool, modal: bool) -> Collect {
Collect {
modal,
..Collect::new(
scope_id(session),
Origin::Current,
if backwards { -1 } else { 1 },
)
}
}
pub(super) fn exact(session: ModelId, generation: ModelId, path: &str, step: i32) -> Collect {
Collect::new(
scope_id(session),
Origin::Exact(field_id(generation, path)),
step,
)
}
impl Operation for Collect {
fn custom(&mut self, _: Option<&Id>, _: Rectangle, state: &mut dyn std::any::Any) {
if let Some(owner) = state.downcast_ref::<FormOwner>() {
self.owner_valid = owner
.0
.is_some_and(|owner| scope_id(owner.session) == self.scope);
self.modal = owner.0.is_some_and(|owner| owner.modal);
}
}
fn container(&mut self, id: Option<&Id>, _: Rectangle) {
self.entering = Some(self.inside || id == Some(&self.scope));
}
fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn Operation)) {
let previous = (self.inside, self.translation, self.scrolls.len());
if let Some(inside) = self.entering.take() {
self.inside = inside;
}
if let Some(scroll) = self.pending_scroll.take() {
self.translation += scroll.offset;
self.scrolls.push(scroll);
}
visit(self);
self.inside = previous.0;
self.translation = previous.1;
self.scrolls.truncate(previous.2);
}
fn scrollable(
&mut self,
_: Option<&Id>,
bounds: Rectangle,
content: Rectangle,
offset: Vector,
_: &mut dyn operation::Scrollable,
) {
self.pending_scroll = Some(Scroll {
index: self.scroll_index,
bounds: translated(bounds, self.translation),
capacity: Vector::new(
(content.width - bounds.width).max(0.),
(content.height - bounds.height).max(0.),
),
offset,
});
self.scroll_index += 1;
}
fn focusable(
&mut self,
id: Option<&Id>,
bounds: Rectangle,
state: &mut dyn operation::Focusable,
) {
if !self.inside {
self.outside_focused |= state.is_focused();
return;
}
if state.is_focused() {
self.focused = Some(self.fields.len());
}
self.fields.push(Field {
id: id.cloned(),
bounds: translated(bounds, self.translation),
ancestors: self.scrolls.clone(),
});
}
fn finish(&self) -> operation::Outcome<()> {
if !self.owner_valid || self.fields.is_empty() {
return operation::Outcome::None;
}
let origin = match &self.origin {
Origin::Current if self.focused.is_none() && self.outside_focused && !self.modal => {
return operation::Outcome::None;
}
Origin::Current => self
.focused
.map(|index| index as i64)
.unwrap_or(if self.step < 0 {
self.fields.len() as i64
} else {
-1
}),
Origin::Exact(id) => {
let Some(index) = self
.fields
.iter()
.position(|field| field.id.as_ref() == Some(id))
else {
return operation::Outcome::None;
};
index as i64
}
};
// A form keeps a reachable focus at both boundaries. Initial Tab enters
// the first field; initial Shift+Tab enters the last field.
let target = (origin + i64::from(self.step)).rem_euclid(self.fields.len() as i64) as usize;
let field = &self.fields[target];
let mut bounds = field.bounds;
let mut shifts = BTreeMap::new();
for ancestor in field.ancestors.iter().rev() {
let dx = reveal_axis(
bounds.x,
bounds.width,
ancestor.bounds.x,
ancestor.bounds.width,
);
let dy = reveal_axis(
bounds.y,
bounds.height,
ancestor.bounds.y,
ancestor.bounds.height,
);
// Iced reports rounded drawing offsets, while capacity can be
// fractional. Clamping an already-visible axis can move it a pixel.
let delta = Vector::new(
if dx == 0. {
0.
} else {
(ancestor.offset.x + dx).clamp(0., ancestor.capacity.x) - ancestor.offset.x
},
if dy == 0. {
0.
} else {
(ancestor.offset.y + dy).clamp(0., ancestor.capacity.y) - ancestor.offset.y
},
);
if delta != Vector::ZERO {
shifts.insert(ancestor.index, delta);
}
bounds = translated(bounds, delta);
}
operation::Outcome::Chain(Box::new(Apply {
scope: self.scope.clone(),
inside: false,
entering: None,
target,
field_index: 0,
scroll_index: 0,
shifts,
owner_valid: matches!(self.origin, Origin::Current),
clear_outside: self.modal,
preserve_inside: false,
}))
}
}
fn translated(bounds: Rectangle, delta: Vector) -> Rectangle {
Rectangle {
x: bounds.x - delta.x,
y: bounds.y - delta.y,
..bounds
}
}
fn reveal_axis(start: f32, length: f32, viewport_start: f32, viewport_length: f32) -> f32 {
if start < viewport_start || length > viewport_length {
start - viewport_start
} else {
(start + length - viewport_start - viewport_length).max(0.)
}
}
pub(super) struct Apply {
scope: Id,
inside: bool,
entering: Option<bool>,
target: usize,
field_index: usize,
scroll_index: usize,
shifts: BTreeMap<usize, Vector>,
owner_valid: bool,
clear_outside: bool,
preserve_inside: bool,
}
pub(super) fn exclusive() -> Apply {
Apply {
scope: Id::new(super::REVIEW_MODAL),
inside: false,
entering: None,
target: 0,
field_index: 0,
scroll_index: 0,
shifts: BTreeMap::new(),
owner_valid: true,
clear_outside: true,
preserve_inside: true,
}
}
impl Operation for Apply {
fn custom(&mut self, _: Option<&Id>, _: Rectangle, state: &mut dyn std::any::Any) {
if let Some(owner) = state.downcast_ref::<FormOwner>() {
self.owner_valid = owner
.0
.is_some_and(|owner| scope_id(owner.session) == self.scope);
self.clear_outside = owner.0.is_some_and(|owner| owner.modal);
}
}
fn container(&mut self, id: Option<&Id>, _: Rectangle) {
self.entering = Some(self.inside || id == Some(&self.scope));
}
fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn Operation)) {
let previous = self.inside;
if let Some(inside) = self.entering.take() {
self.inside = inside;
}
visit(self);
self.inside = previous;
}
fn scrollable(
&mut self,
_: Option<&Id>,
bounds: Rectangle,
content: Rectangle,
_: Vector,
state: &mut dyn operation::Scrollable,
) {
if self.owner_valid
&& let Some(delta) = self.shifts.get(&self.scroll_index)
{
state.scroll_by(
operation::scrollable::AbsoluteOffset {
x: delta.x,
y: delta.y,
},
bounds,
content,
);
}
self.scroll_index += 1;
}
fn focusable(&mut self, _: Option<&Id>, _: Rectangle, state: &mut dyn operation::Focusable) {
if self.owner_valid && self.inside && !self.preserve_inside {
if self.field_index == self.target {
state.focus();
} else {
state.unfocus();
}
self.field_index += 1;
} else if self.owner_valid && !self.inside && self.clear_outside {
state.unfocus();
}
}
}
@@ -163,6 +163,7 @@ where
chooser: Option<&'a Chooser>,
anchor: Rectangle,
on_event: Rc<dyn Fn(ChoiceEvent) -> Message + 'a>,
focus_scope: Option<super::trigger::FormScope>,
}
impl<'a, Message: Clone + 'a, R: text::Renderer<Font = Font> + 'a> Popup<'a, Message, R> {
pub fn new(
@@ -187,8 +188,13 @@ impl<'a, Message: Clone + 'a, R: text::Renderer<Font = Font> + 'a> Popup<'a, Mes
chooser,
anchor,
on_event: Rc::new(on_event),
focus_scope: None,
}
}
pub fn focus_scope(mut self, scope: Option<super::trigger::FormScope>) -> Self {
self.focus_scope = scope;
self
}
fn reconcile(&self, tree: &mut Tree) {
let state = tree.state.downcast_mut::<Interaction>();
let stamp = self.chooser.map(Chooser::stamp);
@@ -250,6 +256,9 @@ impl<Message: Clone, R: text::Renderer<Font = Font>> Widget<Message, Theme, R>
renderer: &R,
operation: &mut dyn Operation,
) {
let mut owner =
super::trigger::FormOwner(self.focus_scope.filter(|_| self.chooser.is_none()));
operation.custom(None, layout.bounds(), &mut owner);
self.content
.as_widget_mut()
.operate(&mut tree.children[0], layout, renderer, operation);
@@ -270,6 +279,35 @@ impl<Message: Clone, R: text::Renderer<Font = Font>> Widget<Message, Theme, R>
shell.capture_event();
return;
}
if is_input(event) && self.focus_scope.is_some_and(|scope| scope.modal) {
super::trigger::own_input(&mut self.content, &mut tree.children[0], layout, renderer);
}
if let Event::Keyboard(keyboard::Event::KeyPressed {
key: keyboard::Key::Named(keyboard::key::Named::Tab),
modifiers,
..
}) = event
&& let Some(scope) = self.focus_scope
&& super::trigger::tab(
&mut self.content,
&mut tree.children[0],
layout,
renderer,
scope,
modifiers.shift(),
)
{
shell.capture_event();
shell.request_redraw();
return;
}
super::trigger::measure(
&mut self.content,
&mut tree.children[0],
layout,
renderer,
*viewport,
);
self.content.as_widget_mut().update(
&mut tree.children[0],
event,
+39 -22
View File
@@ -188,7 +188,7 @@ fn article(topic: Topic) -> Article {
"Choose New in the top bar. Use Project properties to name the model and describe its purpose. Apply changes commits the property draft.",
"On Architecture, choose Create architecture view. Architecture · allocated work shows work inside its performer.",
"Expand the diagram's side palette to see tool names. Choose Entity, click its position in the diagram, enter Rescue coordinator and press Enter. Escape cancels the draft. Use Actor for an external participant such as a Vessel in distress. Right-clicking a blank area also offers Create Entity here.",
"Right-click Rescue coordinator and choose Add activity. Enter Assess incident and press Enter to create the activity and its performer allocation together. Ordinary Activity creation in the palette leaves work unallocated; use + Relationship with Allocation to assign existing work. The allocated-work view shows work inside its performer.",
"Right-click Rescue coordinator and choose Add activity. Enter Assess incident and press Enter to create the activity and its performer allocation together. Ordinary Activity creation in the palette leaves work unallocated. In the allocated-work view, drag existing work into exposed space inside its performer to assign it; the highlighted performer and status explain the proposed change.",
"Use Add existing to place an element that is already in the model. The initial choices match this view; no search is required. Include already shown offers Locate for elements already placed here.",
"Add a second activity for another participant. Select an activity, choose + Relationship, select Interaction and choose the compatible target. Name the exchange after what is conveyed.",
"Visit Model checks to review unfinished work. Save all writes the accepted model and applied drafts to your chosen file.",
@@ -263,7 +263,7 @@ fn article(topic: Topic) -> Article {
&[
"Choose the Operational or System perspective. Create participants using Entity/Actor in OA or System/System actor in SA.",
"In the diagram's side palette, choose Activity in OA or Function in SA, click its position, enter a name and press Enter. Escape cancels without creating an element. Select a card and press F2 to rename it in place. For semantic decomposition, use Model explorer, select existing work and use + In selected to create smaller work units.",
"Right-click a participant and choose Add activity or Add function to create new work and its allocation together. To assign existing leaf work, use + Relationship with Allocation from the participant to the work. Model explorer also offers + Allocated when a participant and the appropriate work kind are selected.",
"Right-click a participant and choose Add activity or Add function to create new work and its allocation together. In Architecture · allocated work, drag existing work into a performer to assign it, between performers to reassign it, or onto empty canvas to remove its direct assignment. + Relationship with Allocation and Model explorer's + Allocated provide explicit alternatives.",
"Choose Architecture · allocated work to review responsibilities. Choose Functional dataflow to review the activity/function hierarchy and its exchanges independently.",
],
),
@@ -289,7 +289,7 @@ fn article(topic: Topic) -> Article {
const {
Article {
summary: "A diagram is a view of the model. Its boxes, positions and nesting reveal selected relationships; moving a box does not redefine engineering ownership.",
keywords: "containment semantic parent hierarchy nested nesting allocation deployment diagram placement arrange fit fold hide zoom resize routing connection bend waypoint detour label reset selective visibility category categories coverage process context duplicate copy items",
keywords: "containment semantic parent hierarchy nested nesting allocation deployment diagram placement arrange next drag Alt+A auto-layout fit fold hide zoom resize routing connection bend waypoint detour label reset selective visibility category categories coverage process context duplicate copy items",
sections: &[
explain(
"Three things to distinguish",
@@ -309,7 +309,7 @@ fn article(topic: Topic) -> Article {
"Use the diagram selector or + View to work with a focused diagram. View properties edits its name, perspective, projection and Saved view visibility. Apply changes commits the view settings.",
"Choose Containment for semantic children, Architecture · allocated work for responsibilities, Functional dataflow for work and exchanges, or Missions & capabilities for outcomes. All relationships is a broader exploration view; OA ↔ SA traceability displays counterparts.",
"Select an element and edit Semantic parent to change ownership. Use an Allocation relationship to change responsibility. Apply changes validates the semantic change.",
"Drag nodes to arrange the current view, scroll to zoom and use Fit to frame its contents. Arrange changes diagram geometry. Export SVG produces a shareable view.",
"Scroll to zoom and use Fit to frame the current view. In allocated-work views, dragging work changes its assignment and placement together. Choose Arrange next drag or press Alt+A to move work without changing responsibility for that one drag; Escape cancels the armed action. Auto-layout arranges the whole view. Export SVG produces a shareable view.",
"Select a diagram node to access Placement, Fold / unfold and Hide. Hide removes its view placement; Delete from model removes model records and requires review of the affected references.",
],
),
@@ -317,6 +317,14 @@ fn article(topic: Topic) -> Article {
"Keep diagrams readable",
"Use separate views for responsibility, decomposition and cross-layer coverage. Place existing elements with Show in this diagram. A placement's Diagram field can relocate it while preserving its identity and artifact references. Do not duplicate a semantic element merely to show it in another view.",
),
walkthrough(
"Move work between performers",
&[
"Finish or cancel property and source edits, then drag an Activity or Function body in Architecture · allocated work. Aim at exposed performer space. Read the highlighted destination and status before releasing; release outside the canvas or press Escape to cancel.",
"The move preserves work ownership and identities. A uniformly assigned work group moves its leaves together; a group distributed across performers asks you to choose a leaf. Role responsibilities remain explicit: an outward drag that would leave a role-derived performer explains why it cannot remove that responsibility.",
"Make room when headers, labels or other cards would overlap. Move the view or zoom in if the work and destination are not readable. A refused drop keeps the model and history unchanged. One Undo reverses an accepted assignment and placement together; Save all writes the corresponding source changes.",
],
),
walkthrough(
"Save a focused exchange view",
&[
@@ -336,7 +344,7 @@ fn article(topic: Topic) -> Article {
&[
"Select an architecture connection and drag a segment to add a bend. Right-click a line for Add bend here; right-click a circle for Remove bend. With the canvas focused, Insert adds a bend and Delete removes the selected bend. Drag the label independently. Escape cancels a gesture; Undo reverses one completed drag. Shift+F10 opens the selected diagram item's context menu; use arrow keys and Enter to choose an action.",
"Automatic orthogonal routing avoids cards and container headers where space permits. Amber connections need attention: move an obstructing element or a bend. In Select / move, drag a port to any border of its owner. Manual positions are per diagram; right-click Reset automatic position restores connection-aware placement. Ports remain attached to their actual owners. Manual routes are saved separately for each diagram and appear in source, API and SVG export.",
"Right-click a connection and choose Reset routing to clear its pins and label position in this view. The Properties Actions menu provides the same commands. Arrange preserves pins. Moving one endpoint keeps pins fixed; moving a container carrying both endpoints translates their pins. When folding promotes an endpoint, expand it again to resume and edit its saved route.",
"Right-click a connection and choose Reset routing to clear its pins and label position in this view. The Properties Actions menu provides the same commands. Auto-layout preserves pins. Moving one endpoint keeps pins fixed; moving a container carrying both endpoints translates their pins. When folding promotes an endpoint, expand it again to resume and edit its saved route.",
],
),
explain(
@@ -445,28 +453,32 @@ fn article(topic: Topic) -> Article {
walkthrough(
"Describe a path through exchanges",
&[
"First create the activities/functions and their exchanges. Check the source and target work for every exchange.",
"A blank path offers Add first activity/function contribution. You can use existing work or stage a new source in that form; exchanges still need explicit compatible source and target work.",
"Open Behavior & scenarios → New. Choose Operational process in OA or Functional chain in SA, enter its exact name, then Create and open. The new behavior belongs at the layer root and opens immediately; Undo removes the creation as one edit.",
"Open Tools and choose Work or Exchange, then a highlighted path or contribution position to involve an existing source. Each contribution keeps its own identity while referring to the shared engineering definition.",
"In Tools, choose Sequence link and click or drag from the preceding contribution to the following contribution. Choose Tools → Select / reorder to drag a selected path endpoint and reconnect it; a contribution's ↕ handle changes its position within a column. These are local contributions and sequence links, not new architecture exchanges.",
"Select a contribution, then add another after it. Tools → Two paths… creates a split, two paths and their merge in one undoable edit. Enter explicit alternative conditions. Fit overview uses the visible canvas area; 100% keeps working scale with scrollbars. If the minimum zoom is reached, scroll to inspect the remainder.",
"Choose Work (W) or Exchange (E) from the permanent palette on the right, then a highlighted position. Hover a palette button to read its tool name. In Work, Create new source keeps the contribution name and position while you name its Activity/Function and choose its semantic parent. Use this new source stages it; Create commits source and contribution in one Undo. An adjacent exchange may require a particular existing work identity.",
"Choose Sequence link (->) and click or drag from the preceding contribution to the following contribution. Choose Select contributions or paths (S) to drag a selected path endpoint and reconnect it. A contribution's ↕ handle reorders contributions within the same column; links determine the columns. These are local contributions and sequence links, not new architecture exchanges.",
"Select a contribution, then add another after it. Details → Two paths... creates a split, two paths and their merge in one undoable edit. Enter explicit alternative conditions.",
"Use Chain involvement from the Process/Functional chain to the capability it supports. Read Model checks for unfinished or disconnected paths.",
],
),
explain(
"Navigate the canvas",
"The header and palette stay available while Details and creation forms overlay the canvas. Fit frames the contents; −/+ changes zoom and 100% restores working scale. Focus the canvas to zoom with the wheel or pan with middle-button drag or Space + left drag. F6 enters or leaves canvas controls; Tab moves to the next control, Enter/Space activates it, and Escape leaves canvas controls or cancels a gesture.",
),
explain(
"Choose the right behavioral description",
"A chain concentrates on a route through existing exchanges. A scenario can also carry actions, state observations, lifelines and nested fragments. The architecture diagram remains a view of the possible relationships rather than an execution trace.",
),
explain(
"Review the path as an engineer",
"Check that exchanges express meaningful handoffs and that the links and branch guards support the selected outcome. Use Tools → Reuse to involve an existing process or chain as a nested reference. For an imported ordered exchange path, Append exchange… continues that path; Convert ordered path to explicit links exposes its implied links for topology editing. Conversion preserves the original order in one undoable change.",
"Check that exchanges express meaningful handoffs and that the links and branch guards support the selected outcome. Use Reuse process / chain (ref) in the palette to involve an existing process or chain as a nested reference. For an imported ordered exchange path, Details → Append exchange... continues that path; Edit path links and guards exposes its implied links for topology editing. Conversion preserves the original order in one undoable change.",
),
walkthrough(
"Connect a particular use of a procedure",
&[
"Choose Focus canvas for more working space; Restore panels brings the explorer and properties back. Select a contribution inside a reusable preview, then open Selection details to read its complete caller and reference identity.",
"In Selection details, use Expand nested reference or Fold nested reference to inspect that instance. Open source or Open contribution definition visits the shared definition; Back to caller returns to the original caller context.",
"Open Tools → Qualified link / assembly… and choose its source and target. Use Enter reference to open an instance's contributions, Up one reference for its caller scope, or Root for the link owner. Search and page through all contributions in that scope, then select the exact endpoint. The first and second uses of a procedure remain different choices even when their work has the same name.",
"Choose Focus canvas for more working space; Restore panels brings the explorer and properties back. Select a contribution inside a reusable preview, then open Details to read its complete caller and reference identity.",
"In Details, use Expand nested reference or Fold nested reference to inspect that instance. Open source or Open contribution definition visits the shared definition; Back to caller returns to the original caller context.",
"Open Details → Qualified link / assembly... and choose its source and target. Use Enter reference to open an instance's contributions, Up one reference for its caller scope, or Root for the link owner. Search and page through all contributions in that scope, then select the exact endpoint. The first and second uses of a procedure remain different choices even when their work has the same name.",
"Choose Precedence to describe order and its guard, Exchange assembly to identify a work exchange between the contributions, or Common work incidence to state that both use the same work. Select the engineering identity when required, then Apply link.",
],
),
@@ -490,19 +502,23 @@ fn article(topic: Topic) -> Article {
"Create a scenario",
&[
"Open Behavior & scenarios → New → Scenario and enter its name. Explicitly choose Activities or Participants in OA, or Functions or System boundary in SA. Add optional context only when needed: this is a reference, while the scenario itself belongs at the layer root. Create and open starts the empty scenario.",
"Use + Lifeline and choose the participant or work it represents. Choices follow the scenario's viewpoint: operational activities, operational participants, system functions or system boundary participants.",
"Choose a message style in the tool selector. Start at a highlighted sequence position on a lifeline, then click or drag to the receiving lifeline. Complete the exchange and details in the form, which retains that sequence position. Escape cancels the gesture.",
"Call + correlated reply creates both occurrences in one edit. Reply to existing call offers valid unanswered calls. A local timer has an expression and a lifeline, with no global exchange. Create and delete messages describe participant lifetime; Model checks also flag risks inside repeated loops.",
"Choose Add action, Observe state, Reuse scenario or Add fragment and click the intended sequence position. Clicking an action or observation on a lifeline also selects that lifeline. Complete any remaining choices before Create.",
"In Select / reorder, click an occurrence and drag its ↕ handle to another position within the same sequence. The ordered list's up/down controls provide another way to reorder. Select an occurrence to inspect its particular use of the exchange, including its message values.",
"Choose Lifeline (L) in the permanent palette on the right, then click the canvas. Choose existing work/participant or Create new source. The explicit viewpoint limits its kinds; choose participant kinds deliberately. Source name and lifeline name are separate, and the source semantic parent is independent of the scenario. No allocation or capability involvement is inferred.",
"Choose a message style from the palette; hover a button to read its tool name. Start at a highlighted sequence position on a lifeline, then click or drag to the receiving lifeline. Complete the exchange and details in the form, which retains that sequence position. Escape cancels the gesture.",
"Call and correlated reply (C/R) creates both occurrences in one edit. Reply to existing call offers valid unanswered calls. A local timer has an expression and a lifeline, with no global exchange. Create and delete messages describe participant lifetime; Model checks also flag risks inside repeated loops.",
"Choose Action (A), State observation (St), Reuse scenario (ref) or Fragment (alt) and click the intended sequence position. Clicking an action or observation on a lifeline also selects that lifeline. Complete any remaining choices before Create.",
"Choose Select / reorder occurrences (S) and drag an occurrence itself to another position within the same sequence. A drag into another fragment operand is refused. Open Details for the ordered list's up/down controls or Properties for the selected occurrence, including its message values.",
],
),
explain(
"Navigate the canvas",
"The header and palette stay available while Details and creation forms overlay the canvas. Fit frames the contents; −/+ changes zoom and 100% restores working scale. Focus the canvas to zoom with the wheel or pan with middle-button drag or Space + left drag. F6 enters or leaves canvas controls; Tab moves to the next control, Enter/Space activates it, and Escape leaves canvas controls or cancels a gesture.",
),
walkthrough(
"Describe alternatives",
&[
"Choose Add fragment, click its sequence position and select ALT, PAR, LOOP or OPT. Enter the branch conditions; the tool creates the fragment and its operands together.",
"Choose Fragment (alt), click its sequence position and select ALT, PAR, LOOP or OPT. Enter the branch conditions; the tool creates the fragment and its operands together.",
"Click the highlighted position inside an operand to place the next message, action or reference there. The form shows its destination and insertion position. Append to this sequence instead explicitly moves the pending insertion to that sequence's end.",
"Choose Reuse scenario and click its position, then bind every referenced lifeline to a compatible local lifeline explicitly. Select the resulting use and choose Open definition. Back to caller returns to that exact use and its viewport after you apply or cancel pending edits.",
"Choose Reuse scenario and click its position, then bind every referenced lifeline to a compatible local lifeline explicitly. Select the resulting use and choose Details → Open definition. Back returns to that exact use and its viewport after you apply or cancel pending edits.",
"Inspect the scenario projection and Model checks. State exactly what each guard means, including units and assumptions where relevant.",
],
),
@@ -543,7 +559,8 @@ fn article(topic: Topic) -> Article {
walkthrough(
"Build a state machine",
&[
"In Behavior & scenarios → New → State machine, enter its name and search for the existing same-layer participant that owns it. Candidate labels show parent context and alias to distinguish repeated names. If the participant does not exist yet, cancel and create it in Architecture first.",
"In Behavior & scenarios → New → State machine, enter its name and search for the existing same-layer participant that owns it. Candidate labels show parent context and alias to distinguish repeated names.",
"If the owner does not exist, Create new owner retains the machine name and first-view option. Choose its participant kind and semantic parent explicitly. Use this new source returns without changing the model; Create and open commits owner, machine and optional view together.",
"Optionally select Create the first state view, then Create and open. The new machine starts without regions or states. For an existing machine, Create diagram places its existing regions and vertices in a saved view; + View creates another independent layout.",
"Choose + Region and click the highlighted machine, or a State/Mode for a nested region. Choose Initial, State, Mode or another kind from + State / symbol, then click its highlighted owning region. Rename the new element in Properties.",
"Choose Transition and drag from a source vertex to a compatible target, or click the source and then the target. In Select / move, drag states or containers to arrange them; a container moves its descendant placements without changing ownership.",
@@ -901,7 +918,7 @@ fn article(topic: Topic) -> Article {
),
explain(
"A new behavior is still a draft",
"Behavior → New keeps the exact name, owner and viewpoint choices outside the model until Create and open succeeds. Cancel or Escape discards only this draft; if a dialog is open, the first Escape dismisses that dialog. New refuses other unfinished property, source or behavior drafts without applying them. Recovery restores the creation form in its original layer, including rejected raw text, for correction or cancellation.",
"Inside a source subform, Back or the first Escape returns to the unchanged parent. Use this new source stages the prerequisite only. Canceling the parent discards it; choosing an existing reference excludes the staged source from creation. Recovery retains both forms and their reserved identities. An open dialog still receives Escape first. Behavior → New keeps the exact name, owner and viewpoint choices outside the model until Create and open succeeds. Cancel or Escape discards only this draft; if a dialog is open, the first Escape dismisses that dialog. New refuses other unfinished property, source or behavior drafts without applying them. Recovery restores the creation form in its original layer, including rejected raw text, for correction or cancellation.",
),
explain(
"Aliases and references",
+2
View File
@@ -1,4 +1,6 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
mod allocation_drag;
mod allocation_input;
mod app;
mod behavior;
mod connection_edit;
+22 -2
View File
@@ -415,7 +415,9 @@ pub fn popup(app: &App) -> Option<Menu<Message>> {
true,
));
entries.push(item("Placement properties", Message::Select(node.id), true));
entries.push(item("Hide from this diagram", Message::HideSelected, true));
entries.push(
item("Hide from this diagram", Message::HideSelected, true).shortcut("Delete"),
);
} else if e.kind() != Kind::Port
&& let Some(d) = app.active_diagram.and_then(|d| p.diagrams.get(&d))
{
@@ -465,7 +467,25 @@ pub fn popup(app: &App) -> Option<Menu<Message>> {
true,
));
if id != p.id {
entries.push(item("Delete from model…", Message::Delete, true));
entries.push(
item("Delete from model…", Message::Delete, true).shortcut(
if request.canvas.is_some_and(|context| context.diagram != id)
&& (request.placement.is_some()
|| p.relations.contains_key(&id)
|| p.elements
.get(&id)
.is_some_and(|element| element.kind() == Kind::Port))
{
if request.placement.is_some() {
"Shift+Delete"
} else {
"Delete / Shift+Delete"
}
} else {
""
},
),
);
}
}
Some(Menu {
+3
View File
@@ -87,6 +87,9 @@ pub enum RowEvent {
ValuePath(String, crate::data_values::FieldPathKey),
ValueBrowse(String, Option<crate::data_values::FieldPathKey>, usize),
ReferenceBrowse(String, String, usize),
ReferenceOpen(crate::forms::reference_chooser::adapter::Open),
ReferenceEvent(crate::forms::reference_chooser::Event),
ReferenceSet(crate::forms::reference_chooser::adapter::Set),
OperationOmission(bool),
OperationRationale(String),
Tab(Tab),
+82 -6
View File
@@ -296,6 +296,51 @@ impl State {
}
Ok(())
}
pub(crate) fn reference_form(&self, project: &Project) -> Option<&Draft> {
let facet = self.active.selected.as_ref()?;
if self.frozen(project).is_err()
|| self.active.tab != Tab::Facts
|| self.active.decisions.get(facet) != Some(&SemanticDecision::Candidate)
{
return None;
}
self.active.candidate_draft(facet)
}
pub(crate) fn reference_draft(
&self,
project: &Project,
address: Option<RowAddress>,
facet: &FacetSelector,
path: &str,
) -> Option<&Draft> {
if self.frozen(project).is_err()
|| self.active_address != address
|| self.active.tab != Tab::Facts
{
return None;
}
self.active.reference_draft(project, facet, path)
}
pub(crate) fn set_reference(
&mut self,
project: &Project,
address: Option<RowAddress>,
facet: &FacetSelector,
intent: crate::forms::reference_chooser::adapter::Set,
) -> Result<(), String> {
self.reference_draft(project, address, facet, &intent.context.path)
.ok_or("The originating review row or property is no longer active.")?
.reference_value(project, &intent)?;
self.update(
project,
Event::Row {
address,
facet: Some(facet.clone()),
event: RowEvent::ReferenceSet(intent),
},
);
Ok(())
}
pub fn update(&mut self, project: &Project, event: impl Into<Event>) {
let event = event.into();
#[cfg(test)]
@@ -303,6 +348,30 @@ impl State {
Event::Editor(event) => self.address_event(event),
other => other,
};
match &event {
Event::Row {
event: RowEvent::ReferenceOpen(_) | RowEvent::ReferenceEvent(_),
..
}
| Event::Editor(RowEvent::ReferenceOpen(_) | RowEvent::ReferenceEvent(_)) => return,
Event::Row {
address,
facet,
event: RowEvent::ReferenceSet(intent),
} => {
let valid = facet
.as_ref()
.and_then(|facet| {
self.reference_draft(project, *address, facet, &intent.context.path)
})
.is_some_and(|draft| draft.reference_value(project, intent).is_ok());
if !valid {
return;
}
}
Event::Editor(RowEvent::ReferenceSet(_)) => return,
_ => {}
}
if let Err(error) = self.frozen(project) {
// Withdrawing a pending reset is authored intent only. Retain it while
// keeping the original accepted base and disabling every stale plan.
@@ -464,7 +533,8 @@ impl State {
);
let scoped_edit = matches!(
event,
RowEvent::Input(_, _)
RowEvent::ReferenceSet(_)
| RowEvent::Input(_, _)
| RowEvent::Text(_, _)
| RowEvent::Shape(_, _)
| RowEvent::Variant(_, _)
@@ -624,7 +694,7 @@ impl State {
Fill,
widget::{button, checkbox, column, pick_list, row, text},
};
let message = |event| Message::Workflow(crate::workflow::Event::Semantic(event));
let message = |event| Message::Workflow(crate::workflow::Event::semantic(event));
let mut available: Vec<_> = self
.base
.traces
@@ -747,13 +817,19 @@ impl State {
if let Some(notice) = &self.notice {
header = header.push(text(notice).size(11));
}
self.active
let view = self
.active
.view(&self.base, header.into(), self.can_apply())
.map(move |message| match message {
Message::Workflow(crate::workflow::Event::Semantic(Event::Editor(event))) => {
Message::Workflow(crate::workflow::Event::Semantic(self.address_event(event)))
Message::Workflow(crate::workflow::Event::Semantic(event)) => {
let event = match *event {
Event::Editor(event) => self.address_event(event),
other => other,
};
Message::Workflow(crate::workflow::Event::semantic(event))
}
other => other,
})
});
crate::forms::reference_chooser::trigger::modal(view)
}
}
@@ -522,10 +522,10 @@ fn batch_apply_is_one_exact_source_and_history_undo_redo_roundtrip() {
&mut app,
RowEvent::Input(path(f.direction, "minimum"), "120".into()),
);
let _ = app.update(Message::Workflow(crate::workflow::Event::Semantic(
let _ = app.update(Message::Workflow(crate::workflow::Event::semantic(
Event::ChooseTrace(view::choice(&f.project, f.traces[1])),
)));
let _ = app.update(Message::Workflow(crate::workflow::Event::Semantic(
let _ = app.update(Message::Workflow(crate::workflow::Event::semantic(
Event::OpenTrace,
)));
tests::decide(
+30 -1
View File
@@ -166,6 +166,14 @@ impl ReviewRow {
pub fn update(&mut self, project: &Project, event: RowEvent) {
let mut replan = true;
let result = match event {
// Browser messages belong to the app overlay and must never replan.
RowEvent::ReferenceOpen(_) | RowEvent::ReferenceEvent(_) => return,
RowEvent::ReferenceSet(intent) => {
let path = intent.context.path.clone();
self.edit(project, &path, |draft| {
draft.reference_set(project, &intent)
})
}
RowEvent::OperationOmission(enabled) => {
self.operation_omission =
enabled.then(|| self.operation_omission.clone().unwrap_or_default());
@@ -518,6 +526,25 @@ impl ReviewRow {
}
}
pub(super) fn reference_draft(
&self,
project: &Project,
facet: &FacetSelector,
path: &str,
) -> Option<&Draft> {
if self.selected.as_ref() != Some(facet)
|| self.decisions.get(facet) != Some(&SemanticDecision::Candidate)
|| matches!(path, "/id" | "/alias" | "/body" | "/layer")
|| !self
.candidate_roots(project, facet)?
.iter()
.any(|root| path == root || path.starts_with(&format!("{root}/")))
{
return None;
}
self.candidate_draft(facet)
}
fn edit(
&mut self,
project: &Project,
@@ -895,7 +922,9 @@ pub fn form_event(message: Message) -> Message {
Message::FieldReferenceBrowse(path, query, page) => {
RowEvent::ReferenceBrowse(path, query, page)
}
Message::FieldReferenceOpen(request) => RowEvent::ReferenceOpen(request),
Message::FieldReferenceEvent(event) => RowEvent::ReferenceEvent(event),
_ => RowEvent::Preview,
};
Message::Workflow(crate::workflow::Event::Semantic(Event::Editor(event)))
Message::Workflow(crate::workflow::Event::semantic(Event::Editor(event)))
}
@@ -3,7 +3,7 @@ use crate::{app::App, document::Document, workflow::Workflow};
use serde_json::json;
pub(super) fn send(app: &mut App, event: RowEvent) {
let _ = app.update(Message::Workflow(crate::workflow::Event::Semantic(
let _ = app.update(Message::Workflow(crate::workflow::Event::semantic(
state(app).address_event(event),
)));
}
@@ -13,7 +13,7 @@ use iced::{
mod reserved_artifact_tests;
fn event(event: RowEvent) -> Message {
Message::Workflow(crate::workflow::Event::Semantic(Event::Editor(event)))
Message::Workflow(crate::workflow::Event::semantic(Event::Editor(event)))
}
fn action<'a>(
label: impl Into<String>,
@@ -867,7 +867,13 @@ impl ReviewRow {
) {
detail = detail.push(text("Attached facts are separate from referenced definitions. Removing membership never implies deleting a semantic object.").size(11).color(style::MUTED));
}
detail.into()
if self.decisions.get(&fact.selector) == Some(&SemanticDecision::Candidate)
&& let Some(draft) = self.candidate_draft(&fact.selector)
{
crate::forms::reference_chooser::trigger::scope(detail, draft.reference_session())
} else {
detail.into()
}
}
fn members_view<'a>(&'a self, project: &'a Project) -> Element<'a, Message> {
+49 -10
View File
@@ -284,7 +284,7 @@ pub fn view(app: &App) -> View<'_, Message> {
)
.padding([4, 16])
.style(style::panel);
let root: View<'_, Message> = column![header, task_nav, body, footer].into();
let root = crate::allocation_input::root(app, column![header, task_nav, body, footer]);
if app.architecture.as_ref().is_some_and(|s| s.modal()) {
return modal_width(
root,
@@ -343,8 +343,8 @@ pub fn view(app: &App) -> View<'_, Message> {
.into(),
);
}
if app.delete_pending {
return modal(root,column![text("Delete from model?").size(21),text(format!("{} and its owned elements will be removed. Incident relationships, occurrences and view placements are updated together. Counterparts in the other layer remain.",p.named(app.selected))).size(13),label("Undo restores the complete change. Use Hide for diagram-only removal."),row![action("Cancel",Message::CancelDelete),button("Delete from model").style(button::danger).padding(10).on_press(Message::ConfirmDelete)].spacing(10)].spacing(16).into());
if let Some(target) = app.delete_target.as_ref().filter(|_| app.delete_pending) {
return modal(root,column![text("Delete from model?").size(21),text(format!("{} and its owned elements will be removed. Incident relationships, occurrences and view placements are updated together. Counterparts in the other layer remain.",p.named(target.snapshot.request.object))).size(13),label("Undo restores the complete change. Use Hide for diagram-only removal."),row![action("Cancel",Message::CancelDelete),button("Delete from model").style(button::danger).padding(10).on_press(Message::ConfirmDelete(target.snapshot.token))].spacing(10)].spacing(16).into());
}
if app.pending.is_some() {
return modal(root,column![text("Keep your changes?").size(21),text("Save the model and pending edits before continuing. Invalid drafts stay available for correction.").size(13),row![action("Cancel",Message::Cancel),action("Discard changes",Message::Discard),button("Save & continue").padding(10).on_press(Message::SaveAndContinue)].spacing(8)].spacing(16).into());
@@ -414,7 +414,7 @@ fn diagram(app: &App) -> View<'_, Message> {
.align_y(iced::Center);
if let Some(id) = app.active_diagram {
chooser = chooser.push(action("View properties", Message::Select(id)));
controls = controls.push(action("Arrange", Message::Command(Command::Layout(id))));
controls = controls.push(action("Auto-layout", Message::Command(Command::Layout(id))));
}
let toolbar = column![chooser, controls].spacing(6);
let projection = app
@@ -442,6 +442,7 @@ fn diagram(app: &App) -> View<'_, Message> {
.align_y(iced::Center);
let canvas: View<'_, Message> = if let Some(id) = app.active_diagram {
let program = DiagramCanvas {
allocation: Some(app.allocation_capability()),
project: app.project(),
view_index: Some(app.document.view_index()),
diagram: id,
@@ -458,18 +459,31 @@ fn diagram(app: &App) -> View<'_, Message> {
connection: app.connection.as_ref(),
};
let mut scene = widget::stack![
widget::canvas(program).width(Fill).height(Fill),
crate::diagram::focus::wrap(
widget::canvas(program).width(Fill).height(Fill),
program.context,
crate::app::diagram_removal::enabled(app),
Message::DiagramTraverse
),
widget::canvas(crate::diagram::PortLabels(program))
.width(Fill)
.height(Fill),
widget::canvas(crate::diagram::WorkFeedback(program))
.width(Fill)
.height(Fill),
];
if let Some(editor) = crate::app::architecture_authoring::inline_editor(app) {
scene = scene.push(editor);
}
row![
widget::sensor(scene)
.on_show(Message::CanvasResized)
.on_resize(Message::CanvasResized),
widget::sensor(
container(scene)
.id(crate::allocation_input::CANVAS_ID)
.width(Fill)
.height(Fill)
)
.on_show(Message::CanvasResized)
.on_resize(Message::CanvasResized),
crate::app::architecture_authoring::palette(app)
]
.height(Fill)
@@ -500,6 +514,27 @@ fn diagram(app: &App) -> View<'_, Message> {
if app.active_diagram.is_some() {
tools = tools.push(modes);
}
if projection == Some(ProjectionKind::Allocation) {
tools = tools.push(
row![
action(
&format!(
"{} · {}",
if app.allocation_drag.arrange_next {
"Arrange next drag: armed"
} else {
"Arrange next drag"
},
crate::allocation_input::ARRANGE_SHORTCUT
),
Message::WorkDrag(crate::app::allocation_drag::Event::ArrangeNext)
),
label("Drag work into a performer to assign · out to remove assignment")
]
.spacing(8)
.align_y(iced::Center),
);
}
if let Some(diagram) = app.active_diagram {
let view = oa_core::projection::project_view(app.project(), diagram);
let edge = view.edges.iter().find(|edge| edge.id == app.selected);
@@ -511,7 +546,9 @@ fn diagram(app: &App) -> View<'_, Message> {
)
.is_some_and(|(source, target)| source != edge.source || target != edge.target)
});
let hint = if let Some(session) = &app.connection {
let hint = if let Some(feedback) = &app.allocation_drag.feedback {
feedback.text.as_str()
} else if let Some(session) = &app.connection {
if session.source.is_some() {
if session.candidates.is_empty() {
"No compatible targets are visible. Expand containers or show an endpoint · Esc cancels"
@@ -528,7 +565,9 @@ fn diagram(app: &App) -> View<'_, Message> {
} else {
"Right-click for actions · drag to arrange · scroll to zoom"
};
tools = tools.push(container(label(hint)).height(30));
// Status is outside the measured canvas. Clipping prevents long text
// from painting over work headers or frozen target labels below it.
tools = tools.push(container(label(hint)).height(30).width(Fill).clip(true));
}
column![container(tools).padding(14), canvas].into()
}
+6 -1
View File
@@ -19,7 +19,7 @@ mod transition_tests;
#[derive(Debug, Clone)]
pub enum Event {
Semantic(crate::semantic_review::Event),
Semantic(Box<crate::semantic_review::Event>),
Port(crate::port_workflow::Event),
ExchangeItem(Id, bool),
Endpoint(bool, EndpointChoice),
@@ -41,6 +41,11 @@ pub enum Event {
Resolve(Id, TransitionField, FieldDecision),
Preview,
}
impl Event {
pub fn semantic(event: crate::semantic_review::Event) -> Self {
Self::Semantic(Box::new(event))
}
}
pub enum Workflow {
Semantic(Box<crate::semantic_review::State>),
Ports(Box<crate::port_workflow::State>),
+17
View File
@@ -2037,6 +2037,23 @@ will occur. Reparenting work must not silently change its performer. A computed
parent summary must remain distinguishable from direct allocation. A collapse
operation hides presentation detail without deleting the children.
In an Allocation view, dragging an Activity or Function body MAY combine a
performer change with diagram placement. The preview MUST identify the
destination and preserve readable work and performer labels. A clean click
selects without applying unrelated drafts. A refused work press with an unapplied
property draft MUST preserve its text, caret and focused editor. An accepted drag is one atomic
model/source/history transaction: semantic ownership and work identities are
preserved; only leaf direct Allocation relations, their own retired routes and
the affected view geometry change. Existing Allocation identities are retained on reassignment. Role
realizations and assignments MUST NOT be silently changed. Uniform work groups
may move their leaf responsibilities together; ambiguous or distributed groups
and changes requiring collateral record edits MUST be refused with an explanation.
Moving outside performer regions removes direct assignment only when the
effective role responsibility is also absent. Releasing outside the viewport
cancels the gesture. Explicit Arrange next drag applies geometry alone for one
gesture. Refused and snapped no-op drops MUST NOT create history entries or
discard Redo. Undo restores the exact source bundle and identities together.
Applying a reachable source bundle is one model transaction. Graphical edits
update its owning declarations through the source-preserving updater. Both
paths validate their candidate and model/source equality before
+1 -1
View File
@@ -22,7 +22,7 @@ Graphical and textual parity applies to the persistent fields of this profile. D
- OA captures operational needs; SA defines the selected system contribution. SA contains no internal logical/physical components, and Studio does not decompose SA Actors. The System may be absent while authoring, with a completeness warning.
- Work decomposition and performer allocation are independent. Only leaves have direct allocations. A parent drawn within one performer can be a computed leaf summary, not an additional allocation. Role sharing does not create several direct performers for one function.
- Nesting expresses semantic containment or the selected allocation projection. Placement movement does not change ownership or allocation. Hide and delete have different effects.
- Nesting expresses semantic containment or the selected allocation projection. A placement edit preserves ownership and allocation. In an Allocation view, a work-body drag previews and commits performer reassignment and placement as one transaction; Arrange next drag (Alt+A) keeps that gesture limited to geometry. Role responsibilities remain unchanged, and drops requiring ambiguous responsibility or collateral edits are refused. Hide and delete have different effects. Current verification and native limits are recorded in [issue 3](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/3).
- Counterpart creation copies mapped typed content and required references. Existing-counterpart semantic review compares typed facts and prior observations, with explicit keep, take or final-value decisions for supported scalar, structural, constructor and individual collection-row changes. Reference mapping and candidate validation apply before an undoable transaction. Ordered collections have explicit final-order review with retained rejected drafts. Included trace rows can combine multiple source contributions into one validated model/history transaction; conflicting decisions and incompatible reference mappings remain explicit blockers. Existing Child/Incident attachment fields and artifact lifecycle review are implemented. Artifact creation reserves an explicit counterpart identity and requires all seven metadata decisions; identified source-owner absence can delete a link, while destination-only Take retains it. Owner moves and kept Presence must agree. Latest attachment/artifact native acceptance remains pending. Review does not automatically synchronize arbitrary structure or overwrite the other perspective. Creation scope follows ownership/reference dependencies, not every incident relationship.
- Processes/chains use contributions and explicit sequence links; imported ordered exchange paths retain their implied order until topology conversion. Rich fragments belong to scenarios. Guards, expressions and state behavior text are descriptive; there is no execution engine. Mixed states and modes are permitted by this Studio profile.
- OA Communication means connect Entities/Actors directly and can allocate Interactions whose actual performers match their endpoints and direction. Transition to SA requires explicit compatible boundary-port decisions; the tool does not guess system ports. Preserved OA boundary exchanges use Legacy ports. Capability grouping is containment; include, extend and generalization use separate typed relationships in OA and SA. Data/item references remain within their layer and can have explicit transitioned counterparts.
+13 -76
View File
@@ -1,79 +1,16 @@
# First release execution plan
# Historical execution plan — superseded
Updated 2026-09-11. [PROJECT_GOAL.md](../PROJECT_GOAL.md) is the authoritative
current objective. [STATE.md](STATE.md) records exact execution/evidence and leases;
[TASK_LEDGER.md](TASK_LEDGER.md) separates facts, assumptions and decisions.
The complete previous plan is [preserved byte-for-byte](history/2026-09-10-ledgers-before-continuation-refresh/PLAN.md).
Its earlier goal paths, active-process claims and Escape pauses are historical.
Live plans, ownership, dependencies and handoffs moved once to
[Gitea issue1 comment412](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/1#issuecomment-412)
and the linked canvas issue on2026-09-11, following the shared working agreement.
This document is retained as a stable navigation target, not a live ledger.
## Outcome and working principles
The [complete original PLAN.md](history/2026-09-11-ledgers-before-gitea-migration/PLAN.md)
is byte-preserved with the [archive manifest](history/2026-09-11-ledgers-before-gitea-migration/manifest.json).
Its decisions, rationale, examples and historical evidence remain available;
relative links inside it resolve from the original `harness/` directory.
Deliver usable, complete OA/SA engineering workflows on Windows and Linux, with
one precise reST contract and equivalent graphical, textual and REST editing.
Constraints/calculations, quantities/units and reusable libraries are explicit
requirements alongside behavior/state modeling, traceability and navigable help.
Small and large models must be practical to author entirely in uncluttered text;
evaluate complete authored and stabilized models, not abbreviated grammar sketches.
Later Arcadia layers remain future scope. Compilation, metadata fields and polished
screens alone do not satisfy this outcome.
Retain the shared typed core, stable identity, semantic ownership/allocation distinct
from geometry, validated atomic commands and exact source/history. Persistent
extensions need coherent references/deletion, transition, source/migration,
API/schema and reachable graphical authoring. Define observable acceptance before
broad downstream changes. Do not silently promote a design proposal to a decision.
## Capability milestones
| Milestone | Work and acceptance | Dependency/current limit |
| --- | --- | --- |
| 1. Reliable direct authoring | Exercise actual minimum/wide native creation, allocation, editing, rejection, cancellation and history without implicit draft application or placement drift. Integrate compact named behavior creation after its coherent external gate. | Named creation4bb4aaf is imported; combined desktop676/676, format and strict Clippy pass at2cc4d11. Native80cf197 passes all four OA creations/history/reopen; new workspace fixes and production chooser await compilation. Workspace/B3/palette/recovery/packages are imported. Root owns this host's Cargo/native lease. |
| 2. Coherent process/scenario work | Interactive ordered/explicit process editing; clicked root/nested scenario insertion; exact reuse bindings and caller return; measured canvas/zoom/scroll; visible scoped Create/Delete lifetimes. Validate model/source/identity and no unintended edit during navigation, including stale/deleted/retargeted references and recovery. | B1/B2/B3 regressions pass in the full suite. Local B4 remains cfg(test); Codex-ytr83r owns production Scene integration and its independent native gate. Use the independently self-checked oracle for native checkpoints. |
| 3. Precise engineering analysis | Compare owned properties, analysis contexts and existing-data/external alternatives; settle quantity kind/unit/value, numerical policy, calculation/binding and Boolean/error/unknown semantics through falsifying examples. Implement mixed-unit budgets and reusable calculation/constraint definitions with full UI/source/REST parity and exact history/recovery. | B/shared-check/full-capture and R6/R7 architecture ratified after complete fixtures and criticism. Exact numeric/work-meter foundation is undeclared and under independent review. Approximate profile and full evaluator parity remain required. |
| 4. Maintained reusable libraries | Compare package/definition/instance alternatives. Establish pinned identity/version/dependency closure, local edits, import bindings, detach and explicit three-way updates. Demonstrate two projects, two independent instances, offline reopen, conflicting updates and stable unaffected identities. | Exact pins plus verified same-origin typed compatibility, explicit source preparation/acceptance and three-way editable updates are ratified. Finite120-assertion experiment is not production contract/archive/consumer implementation. |
| 5. Understandable complete engineering tasks | Close teaching, OA↔SA transition/reconciliation, typed state/data/interface, artifact, saved-view and hosted workflows. Establish complete text-only creation/refactoring/modular maintenance with stable identity. Replace concrete long homogeneous selectors with contextual/searchable bounded choices; verify large models, keyboard access, minimum sizes and failure/recovery. Keep offline guide/examples aligned. | A complete warning-free text-only model probe passes source/identity/rename/reparent/file-move checks; measure usability separately. Anchored chooser passes13 isolated mounted tests under cfg(test); production inspector/review integration is active; its compile/native gates remain open. Separate-host CLI is coherent at01b76a1 and awaits import here. Reassess current goal rather than copying historical scope exclusions. |
| 6. Release confidence and delivery | Fresh current Windows/Linux suites, strict Clippy/format, normative reST/OpenAPI/migration/examples; actual GUI/service transport, optimized builds, runtime/notices/archive/provenance/checksums and clean-environment launch. Fresh independent requirements/domain/parity/architecture/UX/runtime review; resolve material findings, then final handoff. | Integrates all preceding capabilities. Linux runtime remains open; use an approved supported environment, preserving historical failed/denied experiments. Historical previews are not final distributions. |
Milestones are capability gates, not a rigid serial schedule. Root can continue
build/native acceptance while independent foundational investigations compare
alternatives. Libraries and analysis share definition-resolution assumptions and
must converge coherently before broad model/schema edits. User workflows and
cross-representation evaluators guide implementation boundaries.
## Immediate execution sequence
1. Complete the coherent workspace/production chooser build and focused gates,
then stage a new GUI for real engineering tasks. Full676/format/Clippy passed
for2cc4d11; working changes require their own verification.
2. Exchange exact verified commits with the separate host, preserving its workspace
ownership. Do not merge the bundle-transfer branch or assume remote publication.
3. Use the self-checked [scenario oracle](scenario-authoring-contract.rs), follow
its [native procedure](fixtures/scenario-authoring/README.md), and compare exact
whole snapshots for Undo/Redo/reopen at minimum/wide sizes.
4. Integrate and measure the exact numeric/unit/work-meter foundation, resolve
independent findings, then extend actual model/source/API/GUI under the ratified
B/R6/R7/full-capture and exact-pin/typed-compatibility contracts. Preserve explicit
approximate numeric, complete consumer/archive and two-project release gates.
5. Update current ledgers and durable decisions when evidence changes the plan.
Reassess unresolved assumptions before expanding downstream implementation.
## Coordination and evidence rules
Use Gitea issue #1 and Wiki for current ownership/handoffs; read before resuming.
Codex-956atf, Codex-9e8r6t and Codex-ytr83r are distinct hosts. Cargo/native leases
are host-local; shared source ownership is coordinated.9e8r6t owns the next runnable
Windows named-authoring prerelease and contextual-source/CLI; ytr83r owns B4 Scene
and its platform gates. Root owns local chooser/numeric/workspace integration.
Updated AGENTS requires runnable milestone prereleases. User#178 authorizes existing
MCP credential reuse for Git/REST; secrets must never be printed or committed.
Direct branch publication replaces bundle work when available; old exact bundles
remain valid evidence and transfer branches must not be merged.
Record exact source/binary, command exit, test scope and native model/source oracle.
Do not add focused reruns to full-suite totals or transfer old binary acceptance.
Older denied Linux workaround instructions remain applicable to that action; do
not bypass them. Older stopped native sessions do not prevent currently authorized
native work. The [archived plan](history/2026-09-10-ledgers-before-continuation-refresh/PLAN.md)
and [archived progress](history/2026-09-10-ledgers-before-continuation-refresh/STATE.md)
preserve all previous strategy and scoped evidence; archive report links resolve
from their original `harness/` directory.
Use [STATE.md](STATE.md) for the verified branch checkpoint,
[DECISIONS.md](DECISIONS.md) for durable decisions, and
[PROJECT_GOAL.md](../PROJECT_GOAL.md) for the unchanged user requirements.
Do not repeat the migration or revive the duplicated live ledger.
+121
View File
@@ -0,0 +1,121 @@
# User-requested reboot checkpoint — 2026-09-11
Codex-956atf `/root`. The user will install Ubuntu26.04 in WSL2 and reboot, and
explicitly requested progress preservation and a safe-reboot signal. Work is
**paused**, not completed. Resume only after the user returns. All three local
agents stopped and saved their work; root stopped its sole Cargo build. No machine
restart, installation, Windows feature change or credential change was performed.
## Saved source and ownership
- Main checkout `codex/behavior-authoring-20260910`: numeric foundation7d14bd0,
CLIbc311bc, corrected external evidence dc0f2e1, instructions ancestryc7ed894,
chooser/workspace7f081ad, staged prerequisites c0e66f9 (externaldf6f595), numeric
evidence/decoder review03d20ec, followed by this reboot checkpoint. Remote branch
is `codex/release1-956atf`. New desktop source is **uncompiled/unaccepted**.
- Isolated `.local/analysis-core-worktree-20260911/`, local branch
`codex/analysis-core-20260911`, remote `codex/analysis-core-956atf-20260911`:
Checkpoint `b96ff3935ab3f36b7144566e46559a05a29292a5`: WIP actual schema8
definitions/Expr/records, commands/references/migration and
static admission, based at7d14bd0. Its
`harness/release-analysis-core-schema8-wip-20260911.md` inventories20 code/test
paths and22 prepared, unexecuted tests. No source8/evaluator/transport closure.
- Isolated `.local/behavior-canvas-worktree-20260911/`, local branch
`codex/behavior-canvas-20260911`, remote `codex/behavior-canvas-956atf-20260911`:
WIP shared camera/viewport, based atc0e66f9. Checkpoint
`e511c4153fb5bb484652434340e0e4c7000c93d0` is deliberately
not compile-coherent: missing test module, missing App Camera handlers and old
production Scrollables still active. Its
`harness/release-behavior-canvas-reboot-handoff-20260911.md` lists exact paths
and ten remaining steps. Do not merge it as a finished canvas implementation.
Both WIP branches are independent of the main branch. Preserve them, finish their
bounded slices and review before integration. No worker may run Cargo/native or
edit the root checkout without a new root lease. Separate-host9e8r6t owns incoming
prerequisite-form refinements; ytr83r owns B4/lifecycle/Linux work. Re-read Gitea
issues1/2 and current Wiki before resuming; their progress may have changed.
## Verified gates and interrupted build
- Numeric26/26 at7d14bd0,0.29s execution; strict core all-target Clippy with
warnings denied passes11m49s. Exact162-input/binary/log provenance is in
[the numeric gate](release-analysis-numeric-gate-20260911.md).
- Local `cargo test --locked -p oa-cli -- --test-threads=1`:12/12 integration
tests, zero failures/ignored,3.00s execution after42.86s compile. The initial
`--bin arcadia` invocation ran zero tests and counts only as compilation.
Full log: `.local/logs/source-cli-full-local-20260911.log`.
- Previous desktop676/676, formatting and strict Clippy passed at2cc4d11.
No acceptance transfers to new7f081ad/c0e66f9 source.
- `cargo test --locked -p oa-desktop --bin arcadia-studio --no-run` was deliberately
stopped for this reboot while compiling oa-core after renderer dependencies.
Cargo exit101 follows the explicitly stopped owned rustc process (`0xffffffff`);
no source diagnostic or test failure was reported, and no desktop tests ran.
Preserve `.local/logs/desktop-chooser-contextual-first-20260911.log`. Root
verified no Cargo/rustc/Clippy processes remained after stopping its process tree.
## Two new focus findings to fix first
[Paused independent integration review](release-chooser-contextual-integration-review-20260911.md)
contains16 source hashes and two source-derived, unexecuted P2 findings:
1. Initial parent FormShown consumes its one-time focus. Child source Open removes
that parent widget tree; Back/Use remounts it without a new focus intent.
2. FormShown validates context before creating a global NAME_INPUT focus Task,
but the delayed operation is not bound to the still-current mount/session.
It can act on a replacement parent or clear current child/Review input focus.
Implement a live-session/mount-scoped focus operation covering initial mount and
source→parent return. Add actual mounted regressions for Open→Back/Use, changed
document/owner/modal/session between Task creation and execution, raw/source/history
preservation, and then continue the combined compile/focused/full/Clippy/native
gates. Do not misstate the popup search-input counterexample: the root Popup
operation traverses its ordinary content; the separate picker input is excluded.
## Native session and exact private backup
Immutable GUI80cf197 remains the native test binary; SHA256
`A5ED6DF334B1BFB620425AFDF3E863F0B522990E6FB47C6C401A71CF4443954A`.
All four OA create/history checkpoints and final reopen pass. Separate SA Action
Save/Undo/Redo passes, with63 accepted objects. Working entry:
`.local/native-qa/scenario-20260911/sa/project.arcadia`.
The current **unapplied** State draft has literal name `SA native state`, captured
by clicking SA A inspector at the inner chosen operand before SA Inner anchor.
Its lifeline is preselected, but SA Ready is not chosen and Create was not pressed.
It must not be described as a passed SA State checkpoint. Old GUI again required
scrolling its new form back upward to find Name; new fixes have not run natively.
Private backup `.local/reboot-20260911-1344/` contains the complete accepted SA
source directory, exact64,935-byte recovery JSON, build/CLI logs and hash manifest.
Recovery id `30511014-b831-47e3-afd2-6c4e44835a8f`; copied recovery SHA256
`5C7B90BE613AF35463C563B9AA85A7AB1F8D48561EA935ABC6FC1E79549DF0E8`.
Root verified its project/origin and raw draft name before copying. The original
checkpoint remains under `%LOCALAPPDATA%/ArcadiaStudio/recovery/`. The saved-model
backup passes `check sa-action` and exact `same` against sa-action-verified.
No private recovery data is committed or sent to Gitea.
After reboot prefer the application's Home recovery entry to resume this raw draft.
If needed, preserve any newer checkpoint before restoring the exact private backup
to that same recovery directory. Use the independent scenario oracle for subsequent
State/Reuse/Fragment/history/reopen checks. Old window handles/screenshot ids and
process ids are invalid after reboot; select the actual returned Studio window.
## Linux and resumption
Before installation, this account had WSL2.7.13/kernel6.18.33.2-2 and default2 but
no registered distribution or Ubuntu Appx package. VirtualMachinePlatform was
enabled, hypervisor present, common reboot flags absent; elevated feature detail
was unavailable. No Linux startup/build pass follows from those checks.
On return, inspect `wsl --list --verbose` and actually execute `uname -sr` inside
the installed Ubuntu. Verify the user's installed distribution/version, then set
up the project's Linux dependencies and perform real Linux build/test/runtime
gates. Keep Windows compilation serialized using `.local/toolchain/env.ps1`.
The isolated core's first proposed gate is its22 prepared analysis tests, plus
numeric regressions, after review of its WIP handoff. Full depth128 JSON decoding,
allocation/lifecycle/output caps and exact save/readback remain required; see
[the decoder review](release-analysis-decoder-review-20260911.md).
Continue the original complete OA/SA release goal, preserving issue2's modern
canvas requirement and analysis/library/source/REST/GUI parity. This reboot pause
does not alter completion criteria or authorize marking the project complete.
+76 -163
View File
@@ -1,178 +1,91 @@
# Progress Ledger — first release
# Verified branch checkpoint — 2026-09-12
Updated 2026-09-11, coordination through Gitea #190. This ledger describes the
Codex-956atf checkout; external-host evidence is labeled separately. The entire
previous ledger is preserved [byte-for-byte](history/2026-09-10-ledgers-before-continuation-refresh/STATE.md).
Historical dates, binaries, process IDs and stopped-session instructions in that
archive are evidence history, not current status or authorization.
This checkpoint describes Codex-956atf's contribution, not a combined release.
[PROJECT_GOAL.md](../PROJECT_GOAL.md) remains authoritative; the complete OA/SA
release is unfinished. Current user guidance emphasizes close study of Capella
semantics and practical, ergonomic engineering workflows.
## Current objective and source
**Linux/WSL builds and all checks are paused until the user explicitly reports
networking resolved.** No guest diagnosis, restart or cache transfer is active.
Completed Linux evidence remains historical; the VPN remains untouched.
Deliver the complete, usable OA/SA first release in [PROJECT_GOAL.md](../PROJECT_GOAL.md),
including graphical/source/REST parity, constraints/calculations, quantities/units,
reusable model libraries, traceability, offline guidance and Windows/Linux release
acceptance. Direct behavior/scenario authoring and reduced cognitive load remain
immediate priorities. Complete small and large models must be practical to develop
through uncluttered text alone. The release is incomplete.
## Verified implementation
Local branch: `codex/behavior-authoring-20260910`. Last compiled source checkpoint
is `2cc4d115d0ff28423c7ed87fd5f4065efb011f2d`, including external named creation
`4bb4aaf` as `1516949`, the isolated chooser and final baseline/library contracts.
Full desktop676/676, formatting and strict Clippy pass at that source. The active
native GUI remains immutable80cf197. New workspace and production chooser changes
are outside those artifacts and await a coherent build. Numeric foundation files
are undeclared and under independent review.
The authoritative goal was copied byte-identically from attachment
`a49d1780-25d5-4778-ad68-6140c60e4207/goal-objective.md`; SHA256
`DD58F75C93D55FE16ED141A23D834898FB6EBC625AE7E7AE379DA552A8312435`.
See [Task Ledger](TASK_LEDGER.md) for knowledge/decisions and [Plan](PLAN.md)
for capability acceptance. Older goal paths are superseded.
Previous native acceptance uses source **3c9e6eeb64ce3efc03b29890b82199a42c21f335** on
`codex/behavior-authoring-20260910`. It passes 720/720 Windows desktop tests and strict
all-target desktop Clippy. It includes direct scenario/process authoring,
parent-focus and editing-session/chooser corrections, and boxed workflow events.
Evidence checkpoint e7491fb preserves exact logs. Instruction-only merge828c50c
retains the incoming user-owned AGENTS/PROJECT_GOAL files unchanged. The actual
remote default branch `main` was checked after that merge; its current guidance
was read without overwriting the user's local specification.
## Completed work and evidence
The matching GUI built warning-free and passed recovered SA State, clicked Reuse,
and ALT Fragment creation with exact Save/Undo/Redo and final native Open/Save at
the 1100x700 client minimum. The whole Project, all identities and both source files
match every required history checkpoint. The native session is closed.
The creation bundle was fetched anonymously and verified at105022 bytes,
SHA256 `0DEFAE91A56430076C432758F923BC0675911DA9FA7588C9961FF15F47EAEBF6`.
Its coherent commit adds named Process/Chain/Scenario/Machine drafts, bounded
owner search, recovery/selection guards, offline guide and measured textual
examples. The duplicate Sensor correction was not reapplied. Both hosts' report
additions and exact experiment bytes are retained; this host's goal and ledgers
remain authoritative locally. External653-test/native evidence is attributed in
[its integrated report](release-integrated-gates-20260910.md), not claimed as a
local run. The separate isolated chooser at `c4e1580` passed13/13 mounted tests
under cfg(test); it is not a production selector yet.
- [Windows regression/lint evidence](release-chooser-correction-gate-20260911.md)
- [Previous native SA evidence](release-scenario-native-3c9e6ee-20260911.md)
- [Earlier native OA/SA Action evidence, scoped to 80cf197](release-scenario-native-20260911.md)
- [Scenario oracle and reproducible procedure](fixtures/scenario-authoring/README.md)
| Scope | Current evidence and limits |
| --- | --- |
| Reproducible local toolchain | Ignored `.local/toolchain/env.ps1` selects Rust/Cargo 1.98.1 GNU/LLVM and verified LLVM-MinGW 20260908 (clang 23.1.1), with one Cargo job. First cold desktop test build completed in 58m03s; one unused import warning was corrected separately. |
| B1 process authoring | `01bb52a` includes interactive empty/ordered/explicit topology and presentation-only inferred paths; first conversion preserves ordered adjacency atomically. All topology regressions pass within the current full desktop suite. Native acceptance is pending. |
| B2 scenario authoring | Clicked root/operand/slot and optional lifeline feed Action/State/Reuse/Fragment adapters. Pointer-to-App, cancellation, stale-event, recovery and history tests pass within the current full desktop suite. |
| B3 caller navigation and workspace | External `3978bbe` imported as `865cd55` fixes visible-owner restoration, pending-draft guards and remaining-height workspace. Root `80cf197` keys the viewport Sensor by document/scenario; a real mounted regression failed before the fix and now passes. The full suite is green; actual OA caller return and source-neutral navigation pass. Remaining native scope is explicit below. |
| Architecture palette | External palette commit `7d1df4f` was cherry-picked as `aed40d7`, retaining local invariant tests. Captured Escape, stale Submit/raw retention and bounded card fixes are source-integrated. External native results do not transfer to this checkout. |
| Typed stored-history F1 | `b6b76ef` fixes nullable constructor cache discrimination; `4ecad78` fixes NotApplicable seeding by checking actual enum alternatives. Thirteen focused tests passed and are included in the current full suite; core validation remains authoritative. |
| Lifeline lifecycle B4 | `eb5161b` adds pure local/scope-aware lifecycle projection with ten tests, declared under `cfg(test)` only. It is not production Scene wiring or native acceptance. [Design](release-scenario-lifecycle-design-20260910.md). |
| Native scenario oracle | [Independent checker](scenario-authoring-contract.rs) compiled/self-checked and verified the generated62-element/two-source baseline. Native OA Action/State/Reuse/ALT-fragment, each exact Save/Undo/Redo, and final native reopen pass at minimum size; caller identity/scroll return and source-neutral navigation pass. Remaining prescribed checkpoints are open. [Native scope](release-scenario-native-20260911.md), [oracle inputs](fixtures/scenario-authoring/README.md). |
| Normative documentation | Strict Docutils 0.22.2 language HTML regeneration passed. `semantic_schema.py --check --validate` passed 72 source7 fixtures, 204 schemas, 14 families/90 scalar leaves. These are documentation/schema gates, not a Rust build or runtime pass. |
| Comparable-product study | Actual isolated Capella 7.1 scenario/activity/lifeline/LOOP creation was saved and XML inspected, then closed normally. [Report](release-capella-scenario-benchmark-20260910.md). Current Studio native evidence is recorded separately. |
| New requirement audit | [Independent delta report](release-requirements-delta-20260910.md) establishes that current expressions/unit labels are descriptive and source includes are not maintained cross-project libraries. Refined B/R6/R7 and exact-pin/same-origin compatibility contracts are ratified; current source7 does not implement them. |
| Packaging integration | External `b98f135` imported as `6d2b961`; all 25 checked-in exact notices match the manifest. `scripts/test-package-artifacts.py` passes 11/11 and `scripts/test-windows-notices.ps1` passes synthetic package/runtime/source/font checks. Actual optimized distributions and clean-environment launches remain open. |
## Isolated contributions and material limits
## Active work, ownership and unresolved findings
Core candidate **847b1caa7e75bb82f6ce5193c45bcfc45973ad1a** is on the separate
`codex/analysis-core-956atf-20260911` publication branch. Windows execution passes
the 128 KiB-thread lifecycle subprocess, all 6 codec tests, 28 analysis tests, 26 numeric
tests and strict all-target core Clippy. These counts are separate scopes; the
subprocess driver is included in the 6. Production `DEPTH_128_VALIDATED` remains
false. Legacy/migration/save integration, release memory measurements and complete
source8/transport/evaluator acceptance remain open.
[Windows codec evidence](release-analysis-codec-windows-gate-20260911.md).
Root Codex-956atf owns this host's integration, serialized Cargo and native UI.
The source2cc4d11 gate completed with exit0: **676 passed, zero failed/ignored**,
tests611.24 seconds after warning-free compilation10m47s. Command:
`cargo test --locked -p oa-desktop --bin arcadia-studio -- --test-threads=1`.
Log `.local/logs/desktop-creation-chooser-20260911.log`. Immutable executable
`.local/creation-chooser-20260911/tests-2cc4d11/arcadia-studio-tests.exe`, SHA256
`8D7FB35071F155B3BD07CE1885A1136B2D2EF71C4FF8C704665202499C86A721`.
Its manifest pins256 tracked input hashes. Format passed; strict all-target desktop
Clippy passed7m18s after the isolated manual-clamp correction. Final logs are copied
beside the immutable binary. These gates supersede the historical642 total, not the
current uncompiled working changes. Focused counts are included, never additive.
The cleaned R123 canvas at **a31b40d0f8551ed87c8cd66250b9afb4809b3a79** passes
729/729 Windows desktop tests, strict all-target Clippy and the matching GUI build.
All 518 input identities agree with Git and the closed receipts. Actual minimum-size
OA action, nested state, bound reuse and ALT creation passed Save and exact one-step
Undo/Redo Project/SourceBundle comparisons. All 85 archived files match their Git
index bytes. Reopen was interrupted by physical Escape and remains unverified;
Computer Use remains paused. [Exact gates and bounded native evidence](release-behavior-canvas-a31b40d-native-20260911.md)
supersede the earlier pre-cleanup gate as this source's checkpoint.
Actual native Studio remains `.local/native-qa/studio-80cf197/`, GUI SHA256
`A5ED6DF334B1BFB620425AFDF3E863F0B522990E6FB47C6C401A71CF4443954A`.
That GUI build passed3m28s. At1100x700, all four OA creation checkpoints and final
reopen now pass complete Project/two-source prediction and exact UUID restoration.
Preserved snapshots end in `oa-reopened/`; no SA or new-fix acceptance is inferred.
[Native report](release-scenario-native-20260911.md) records details and two real
workspace findings: new forms retain downward scroll, and opening a form can hide
its clicked marker below the reduced canvas. Root has written a fresh form-session
key, guarded Name focus, one-time marker reveal and three meaningful mounted/App
regressions. They are not yet compiled. The isolated chooser's corrected realistic
typing/paste/IME gate passes13/13; production inspector/review integration is now
owned by local behavior_audit, with exact row/facet/group/attachment guards.
Allocation candidate **f52d81c1b96a34bb78b8ada27b7761dc0b009f5d** preserves the
canvas source and passes its first 54 focused Windows planner/App/widget tests,
including four actual App-tree focus/keyboard/render cases; all 525 inputs remained
unchanged. This is not a full-suite, strict-lint, native or release gate. Independent
review found a frozen performer label can still obscure the dragged work header;
that repair and broader integration remain unverified. [Issue 3](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/3)
owns its live work. Parent/child native focus, complete direct manipulation,
persistent process placement/lanes and coherent peer integration remain open.
The [baseline contract](release-analysis-baseline-contract-20260911.md) supplies a
complete17-UUID proposed model,11 exact fixtures, explicit ModelBaseline/Case,
property bases and shared checks. Its transport fixtures are not parser/evaluator
acceptance. The [library contract](release-library-compatibility-contract-20260911.md)
has120 executed finite-prototype assertions, separate-process reopen and11 complete
expected/reopened file comparisons. Final81-file manifest was independently
verified; preliminary output is retained and labeled superseded. All166 experiment
files retain exact Git bytes. Root ratified B/R6/R7/full private capture, nominal
typed compatibility alongside exact pins, explicit source normalization and OA/SA
dispositions in [DECISIONS](DECISIONS.md).
The combined allocation/saved-destination repair baseline **d9b3f0dc91e8882e6efc262c2e8d971998d2694e**
passes 792/792 Windows desktop tests with all 526 source inputs unchanged.
[Exact baseline evidence and independent review](release-allocation-d9b3f0d-windows-20260912.md).
Review found the fixed-label overlap refusal makes a collapsed long-name target
unreachable with an ordinary grab offset. The [feedback decision](release-allocation-feedback-decision-20260912.md)
replaces duplicate canvas labels with a dedicated toolbar identity; that correction
and current strict-lint/native acceptance remain unverified.
The [independent refined review](release-refined-contracts-review-20260911.md)
passes292 fixture/identity checks but finds C1:333 stored AST nodes can expand to
over2billion calls. Request-local dynamic/numeric work accounting is mandatory;
static depth/node limits alone fail. C2 is a finite-prototype threshold-unit gap,
not a passing production type-checker. Local requirements_delta owns undeclared
`analysis_numeric` foundation (exact arithmetic, dimension/unit operations and
terminal work meter), with semantics_critic independently reviewing correctness.
Root owns dependency/module admission and all compilation. Numeric ceiling values
remain provisional pending measured gates. Approximate engineering calculations,
full evaluator/model/source/REST/GUI parity and actual library maintenance are open.
Complete quantities/calculations/constraints, maintained libraries, graphical/
source/REST parity, transitions and engineering-task usability, aligned reST/help,
optimized packages and final independent acceptance remain required. Finite
prototypes and published previews are intermediate results.
Codex-9e8r6t and Codex-ytr83r are separate hosts/teams. The former owns contextual
prerequisite creation, source CLI and next Windows named-authoring prerelease.
Its direct branch `codex/release1-9e8r6t` has coherent CLI checkpoint01b76a1; corrected
evidence2ec72d4 and CLI are not yet imported here. External675 staged-source and
CLI12/183-assertion results remain externally attributed. Codex-ytr83r owns B4
production Scene wiring, independent native oracle and local Linux route. Its
rendering source has independent criticism but no passing build/native gate yet.
Do not duplicate these changes. Preserve root's distinct scenario-scroll/focus hook
when integrating other hosts' small App/behavior bridges.
## Authoritative records
Updated AGENTS d93cddac adds coordinated runnable milestone prereleases. Gitea
user#178 explicitly authorizes programmatic reuse of existing MCP credentials for
Git/REST and relevant commits/tags/prereleases, without printing or committing them.
Direct push setup is in progress on this host;9e8r6t has independently succeeded.
Historical verified bundle exchange remains reproducible: c6d296a bundle on
`codex/behavior-transfer-20260910`,334830 decoded bytes, SHA256
`526A80E2D32ED0E57A7B2372B5F02D4652E8751DFEBA81953C4F045225D6AACB`,
prerequisite1797516. Both other hosts received it. Never merge transfer branches.
Older bundles/logs retain historical scope in Git and the archived ledger.
- [Project overview and historical coordination](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/1)
- [Direct process/scenario canvas work](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/2)
- [Activity allocation drag](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/3)
- [Analysis, codec and source/API integration](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/7)
- [Maintained reusable libraries](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/8)
- [Decisions and reusable evidence](DECISIONS.md)
- [Shared overview](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/wiki/BehaviorAuthoring20260911)
- [Published milestones and their exact limitations](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/releases)
- [Historical Linux gate before deferral](release-linux-core-cli-gate-956atf-20260911.md)
## Remaining gates and highest-value next actions
1. Freeze and compile the new workspace/chooser integration, resolve focused
failures, then build an immutable GUI and verify both native workspace fixes.
The676-test gate is complete for2cc4d11; new changes require their own evidence.
2. Publish coherent checkpoints directly; integrate9e8r6t CLI/corrected evidence
and later staged-source work, then ytr83r B4 only after its coherent handoff.
Preserve ownership and distinguish every external versus local gate.
3. Use the self-checked independent oracle to exercise current Studio at
minimum/wide sizes: named creation, direct process/scenario edits, nested reuse,
caller return, lifeline lifecycle, invalid/cancel/recovery, Save/Undo/Redo/reopen.
Compare complete model/source and exact identities, not screenshots alone.
4. Compile/review/measure the numeric foundation, then implement the ratified
analysis/library contracts with shared-core/source/API/graphical parity. Use real
budgets, unit/kind failures, complete text-only and two-project update workflows.
Integrate the bounded chooser and run its actual keyboard/minimum-size gates.
5. Complete current teaching, transitions, semantic review, state/data, saved-view,
artifact, hosted transport and migration acceptance. Historical scoped passes
remain useful references, never blanket current acceptance.
6. Complete Windows/Linux build/test/native/runtime and optimized package gates,
notices, provenance and clean-environment launch. WSL has no installed distro
here; Linux acceptance remains open. Historical denied guest workarounds must
not be retried or split to bypass review. No host restart is authorized.
7. Run fresh independent cross-disciplinary end-state review, resolve material
findings and reconcile specification/help/coverage before final handoff.
## Reassessment
Meaningful progress continues: concrete interaction defects have been found and
corrected,676 desktop tests pass and all four OA native creation/history/reopen
checks pass. Contracts have converged through independent falsification; numeric
implementation now addresses the exposed dynamic-work risk. The revised semantic
requirements are no longer hidden behind metadata.
Passing older suites or accumulating prose would not close the user problem.
Current highest information gain comes from actual build/native results and
independently criticized foundational examples, while preserving existing identity,
source and atomicity contracts. Replan when those results falsify a proposal.
## Historical evidence access
The [complete prior STATE](history/2026-09-10-ledgers-before-continuation-refresh/STATE.md)
retains exact historical binary hashes, counts, logs, process closure, bundle hashes
and all report links. Links inside the byte-preserved archive retain their original
`harness/` context; resolve those relative report paths from `harness/`.
Useful direct entries: [source7 native](release-source7-native-acceptance-20260907.md),
[teaching](release-teaching-native-20260906.md),
[artifact native](release-artifacts-native-20260906.md),
[coverage baseline](release-current-coverage-20260906.md),
[Linux gate](release-source7-linux-gate-20260907.md),
[Linux cross-build](release-source7-linux-gui-cross-build-20260907.md),
[denied boot experiment](release-source7-linux-server-corrected-gate-20260907.md).
The duplicated live ledgers were migrated once into Gitea comment412. Their
[complete original bytes and manifest](history/2026-09-11-ledgers-before-gitea-migration/manifest.json)
preserve historical assumptions, decisions, experiments, limitations and links.
Links inside the archived documents retain their original `harness/` base.
Do not maintain live plans in this checkpoint or repeat that migration.
+13 -212
View File
@@ -1,215 +1,16 @@
# Task Ledger — first release
# Historical task ledger — superseded
Updated 2026-09-11. [STATE.md](STATE.md) owns current execution/evidence;
[PLAN.md](PLAN.md) owns capability milestones. The entire former Task Ledger is
[preserved byte-for-byte](history/2026-09-10-ledgers-before-continuation-refresh/TASK_LEDGER.md).
Its old goal references, aggregate test claims and stopped-session directives retain
historical scope. New semantic decisions are recorded explicitly in DECISIONS.md;
prototype results retain their separate scope.
Live plans, ownership, dependencies and handoffs moved once to
[Gitea issue1 comment412](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/1#issuecomment-412)
and the linked canvas issue on2026-09-11, following the shared working agreement.
This document is retained as a stable navigation target, not a live ledger.
Current compiled checkpoint2cc4d11 includes external creation4bb4aaf as1516949,
isolated chooser13, and final baseline/library contracts. Local full676/format/
strict Clippy pass; native80cf197 now passes four OA creations, exact history and
reopen. Working workspace/production chooser changes and undeclared numeric files
have no passing gate yet. External results retain their host/source attribution.
The [complete original TASK_LEDGER.md](history/2026-09-11-ledgers-before-gitea-migration/TASK_LEDGER.md)
is byte-preserved with the [archive manifest](history/2026-09-11-ledgers-before-gitea-migration/manifest.json).
Its decisions, rationale, examples and historical evidence remain available;
relative links inside it resolve from the original `harness/` directory.
## Goal and success criteria
Deliver the usable, feature-complete OA/SA first release specified in
[PROJECT_GOAL.md](../PROJECT_GOAL.md). It is the exact copy of the user's revised
`a49d1780-25d5-4778-ad68-6140c60e4207/goal-objective.md`, SHA256
`DD58F75C93D55FE16ED141A23D834898FB6EBC625AE7E7AE379DA552A8312435`.
Success requires coherent explicit semantics, practical engineering workflows,
equivalent graphical/textual/REST behavior, real Windows/Linux runtime evidence,
recoverable authoring, relevant automated evaluators, precise documentation,
reproducible packages and fresh independent cross-disciplinary end-state review.
No known blocker or high-impact finding may remain unresolved. Later Arcadia
layers remain future scope. Code, a passing slice or an attractive screen alone
cannot establish completion.
## Given facts
- Rust/Iced desktop and a deployable REST service serve system engineers. OA/SA
transitions in both directions must be understandable, explicit and useful.
- The revised goal explicitly requires constraints/calculations, quantities/units,
reusable model libraries, state/behavior modeling and strong standardized
specification/API, as well as textual/graphical notation and artifact traceability.
- Human/AI/Git-friendly source and a concrete unambiguous reST specification must
match graphical editing. The source editor needs highlighting/linting; help must
be offline, concise and navigable.
- The newest explicit goal requires uncluttered notation that reduces cognitive
load and supports development of complete small and large models using text alone.
- Direct behavior/scenario diagram authoring and reduced cognitive load are current
priorities. Long homogeneous selectors and bloated persistent creation controls
are rejected. Study actual Capella's useful domain workflows and nesting without
mechanically copying its interaction problems.
- The user authorizes autonomous bounded engineering, native Capella/Studio
evaluation and explicit independent delegation, exclusively with GPT-6 Astra
at effort appropriate to the question. Root owns synthesis/integration.
- Project instructions require Gitea coordination, ownership preservation and
durable handoffs. Current root reserves one Cargo/native operator per host.
Updated AGENTS d93cddac requires coordinated runnable milestone prereleases;
user Gitea#178 authorizes programmatic existing-MCP-credential reuse for relevant
Git/REST publication without printing/committing secrets. Restart and usage reset
remain unauthorized.
## Verified facts
- The independent scenario checker compiled with warnings denied and passed its
self-check, deterministic fixture generation and complete baseline check on this
host. Its eight OA/SA checkpoints and exact UUID/source restoration comparisons
are evaluator evidence, not native Studio acceptance. Exact inputs/hashes are in
[the fixture instructions](fixtures/scenario-authoring/README.md).
- The historical native implementation is `80cf197` on `codex/behavior-authoring-20260910`, following
baseline `1797516`. Local commits cover B1/B2/B3 authoring, nullable history cache
(`b6b76ef`), external palette integration (`aed40d7`), cfg(test) lifecycle projection
(`eb5161b`), history seed correction (`4ecad78`) and exact-identity native oracle.
Later imports add focus recovery, exact packaging notices and scenario workspace/
owner restoration. Root80cf197 fixes document/scenario viewport Sensor identity
with a real mounted red-before/green-after regression.
- This fresh host has task-local Rust/Cargo 1.98.1 GNU/LLVM and verified LLVM-MinGW.
The integrated2cc4d11 desktop suite passes676/676, zero failed/ignored, with a
warning-free build; formatting and strict Clippy pass. History, pure lifecycle,
creation and isolated chooser are included. New working code still needs its
own gate. Native80cf197 evidence is separate. [Exact hashes/logs](STATE.md).
- Strict language HTML regeneration and the source7 OpenAPI checker passed locally:
72 fixtures, 204 schemas and 90 scalar leaves. Actual isolated Capella scenario,
activity/lifeline and LOOP creation was saved/inspected; this is reference study,
not Studio acceptance. [Native benchmark](release-capella-scenario-benchmark-20260910.md).
- Core/source7 already implements substantial direct behavior, typed data/ports,
contextual chain links, maintained message-operation identity, semantic reviews,
artifacts, hosted source envelopes and recovery. Historical absence claims must
be checked against code and newer scoped evidence, not copied forward.
- [Requirements audit](release-requirements-delta-20260910.md): Constraint and other
expression strings are descriptive, with no calculation/binding/result evaluator;
unit strings persist/inherit without dimensional compatibility or conversion;
named types/sequence/scenario reuse and includes exist, but there is no maintained
cross-project package/version/instance/update contract. These are explicit gaps.
- The same audit identifies concrete unbounded Constraint target choices and generic
expanded collection editors. Explorer already has hierarchy/search/paging; the
problem is targeted, not a claim that all selection is flat.
- Gitea #79's same-selected-ID/visible-owner defect is fixed in the imported
scenario checkpoint; owner restoration and pending-draft guards pass current
tests. B4 independent review #114 adds Scene integration requirements for actual
lifetime extents, branch-local activation clipping, incomplete lanes and self arrows.
- Codex-9e8r6t and Codex-ytr83r have independent host-local Cargo/native leases.
Named creation/workspace/palette/package integration is imported here.9e8r6t's
coherent CLI/evidence checkpoint01b76a1 awaits import; it owns staged-source and
the next runnable Windows prerelease. ytr83r owns production B4 and its native
oracle.9e8r6t successfully reused existing credentials for direct push; this
host is setting up the same authorized publication path.
- Current Linux acceptance is open; this host has no installed WSL distribution.
Historical Linux core/server/cross-build results have exact older snapshot scope.
The earlier denied guest workaround did not run and must not be bypassed.
## Assumptions
- Compact contextual tools and an adequately sized scenario workspace can reduce
friction; only actual engineering tasks at minimum/wide sizes can validate this.
- The ratified bounded pure analysis and pinned-library architecture can satisfy
real engineering work without full language/runtime adoption. This remains a
product adequacy assumption until actual budgets, approximate calculations and
two-project/source/API/native workflows pass; metadata is insufficient.
- Existing shared core/source/history boundaries remain suitable for additions;
foundational examples and independent criticism must test that assumption.
## Open questions
- Do the ratified owned-value/shared-check/R6/R7 contracts meet real engineering
budgets once implemented, including approximate numeric requirements? What
measured ceilings keep work bounded without rejecting ordinary studies?
- Does actual package closure/canonical consumer typing/source preparation and
three-way update preserve every stable identity and local engineering edit?
- What migration and OA↔SA treatment follows from those contracts without guessing
executable meaning from old strings or silently changing old models?
- How can text-only creation establish library instance bases/maps, preserve
compatibility across cosmetic package revisions, export subjectless cases, and
transition mounted OA/SA data without hidden GUI normalization or silent forks?
- Will full evaluator and private-capture replay preserve effective-case and
deterministic work-limit behavior across direct, REST and cache-warm execution?
- Which source-preserving refactoring/navigation and grammar changes reduce actual
engineering effort while retaining stable identity and cross-file ownership?
- Do current real gestures, exact caller return, nested scopes and lifecycle markers
remain usable and correct under minimum sizing, scrolling, zoom and dirty drafts?
- Which uncovered teaching, traceability, transition, saved-view, state/data, hosted
and platform cases remain after current-binary acceptance? What supported Linux
environment can complete the remaining actual runtime gate?
- Does "standardized" require any external conformance beyond the project's precise
interoperable spec/API? Full OMG conformance is not inferred merely from comparison
to SysML v2; explicit claims require a mapped and verified contract.
## Candidate hypotheses / designs
The [requirements delta](release-requirements-delta-20260910.md) compares metadata/
snippets, bounded pure engineering analysis with maintained pinned libraries,
external evaluators, and full standards adoption. Metadata alone fails the explicit
new semantics; external tools alone still need binding/result contracts. The report
recommends the bounded route for root synthesis, not as a ratified decision.
Independent maximum-reasoning tracks now compare owned engineering properties
versus analysis contexts; calculation definitions/applications; numeric/unit rules;
and copy, linked-definition, live-instance or pinned-definition/editable-instance
library architectures. Require concrete falsifiers, source/API migration effects,
identity/history/recovery behavior and independent critique before convergence.
These alternatives converged after concrete experiments and independent review:
B property bases/shared checks/full capture, R6/R7, exact pins with same-origin
typed compatibility and explicit portable normalization are now ratified. Complete
11-file proposed baseline and120-assertion finite library prototype remain distinct
from production admission. Independent292-check review exposed C1 dynamic-work
expansion and a C2 prototype threshold-unit gap; implementation must close both.
The text-only40-identity fixture and13 mounted chooser tests pass. Production
numeric foundation and inspector/review chooser are active, with gates pending.
The subsequent context experiment passed218 assertions: shared property baselines
and one-level overrides reduce repeated input maintenance while retaining explicit
independent freeze. Root selected that B direction for precise contract refinement;
quantity/library/source details remain open. [Decision](DECISIONS.md),
[experiment and limits](release-analysis-context-experiment-20260911.md).
## Decisions retained
- Keep explicit Rust crate responsibilities and one authoritative typed Project
with validated atomic Commands. A thin source CLI reuses the shared core. Separate semantic identity/ownership, allocation/reference/order and
diagram geometry. Forms are closed adapters, not a second metamodel. New persistent
semantics require core/source/UI/API/reference/transition parity. [Decisions](DECISIONS.md).
- Preserve authored source ownership/comments/includes, exact identity and accepted
model/source history. Source writeback must succeed before adoption. Raw invalid,
excluded or rejected drafts survive; navigation/tools must not implicitly accept
another editor. Recovery validates/replans rather than trusting serialized commands.
- Semantic reconciliation uses finite typed facets, frozen baselines, explicit
mappings, actual destination slots and a single atomic candidate. Do not infer
identity by name or apply reviewed rows as sequential independent edits.
[Foundation](release-semantic-batch-foundation-design.md),
[artifact contract](release-semantic-artifacts.md).
- Layer transitions are explicit engineering decisions, not automatic propagation.
Behavior-root navigation must establish the requested owner while preserving
unrelated architecture presentation. Ports/allocation and contextual occurrences
retain their distinct domain meaning. [Source7 contract](release-source7-integration-decision-20260906.md).
- Hosted revision/source envelopes and ETag checks remain atomic; credentials stay
out of persisted models and repository. Linux journal publication retains the
selected no-replace invariant, with measured limits, not an overwrite fallback.
[Journal decision](release-linux-journal-publication.md).
- Repair/evaluate the compact palette informed by native Capella evidence; do not
return to the rejected horizontal toolbar. Lifecycle projection remains isolated
under cfg(test) until coordinated Scene integration and actual acceptance.
- Preserve historical scope decisions as history. In particular, architecture-only
SVG is implemented and behavior SVG was deferred; that is not proof of satisfying
the revised release objective. Reassess older exclusions against current scope.
## Current strategy
Finish the new workspace/chooser compile and native loop; independently review
and measure the numeric foundation before expanding the ratified semantics.
Integrate coherent external CLI/staged-source/B4 checkpoints while preserving
ownership and source attribution. Use independent complete model/source oracles
and exact identity/history comparisons. Publish coordinated runnable milestone
prereleases; close actual analysis/library/source/API/native, cross-platform,
service/package and independent end-state gates before the final handoff.
The [archived Task Ledger](history/2026-09-10-ledgers-before-continuation-refresh/TASK_LEDGER.md)
and [archived STATE](history/2026-09-10-ledgers-before-continuation-refresh/STATE.md)
preserve all older claims, decisions and evidence links exactly. Relative report
links inside those byte-preserved files resolve from their original `harness/`
context. Their historical test totals and process holds are not live status.
Use [STATE.md](STATE.md) for the verified branch checkpoint,
[DECISIONS.md](DECISIONS.md) for durable decisions, and
[PROJECT_GOAL.md](../PROJECT_GOAL.md) for the unchanged user requirements.
Do not repeat the migration or revive the duplicated live ledger.
@@ -0,0 +1,63 @@
# Diagram Delete candidate handoff — 2026-09-12
Frozen, uncommitted implementation on `codex/diagram-delete-956atf-20260912`, isolated worktree `C:\Users\User\Documents\Git\ARCADIA-Studio\.local\diagram-delete-20260912`, HEAD/base `5a70181`. Parent owns issue 15 comment 551, review, Windows verification and integration. No Gitea, Git index/commit, compiler/cargo, native application, WSL or VPN operations performed by this worker. Files are ready for parent review/copy; do not interpret this as verified or complete issue 15.
## Candidate behavior
- Architecture OA/SA and persisted state diagrams: focused canvas Delete hides the selected explicit placement through ordinary `execute(Command::Delete(placement))`; semantic object remains. Shift+Delete opens the existing model-removal dialog. Visible architecture edges/ports and state transitions without explicit placements use confirmed model removal for Delete too.
- Captured `diagram_removal::Intent` carries diagram document/revision/epoch, object and explicit placement. App independently checks clean draft/source/behavior, modal/menu/reference state, selected object/placement, current diagram/page/tool and visible projected membership. Keyboard path never calls `apply()`.
- `diagram/focus.rs` supplies actual Iced Focusable ownership around the architecture/state canvas. It draws the existing behavior viewport style (1 px teal border) while keyboard-focused, without changing layout. It registers for standard focus operations; programmatic focus of any other field unfocuses it. It rejects captured/unfocused/disabled keys and resets at context/window boundaries. The old mouse-only DiagramCanvas.keyboard_focus and StateCanvas.focused gates were removed: the wrapper is the single keyboard authority. Standalone Program tests still exercise already-admitted events.
- Canvas Tab uses ordinary Iced next/previous focus operations. A transient traversal flag rejects subsequent keys in the same native event batch before that task runs, while retaining focus for the operation's traversal count.
- Delete requires selection mode and no active pointer/route/port/pan/work gesture. Existing bend Delete takes priority and a selected invalid bend cannot fall through to object removal. Unsupported modifiers are rejected. Diagram background is never a shortcut target.
- A completed allocation work click retires its epoch before retained canvas state necessarily sees redraw. `diagram.rs` clears only an already-retired local work press before Delete; same-epoch/live work presses remain guarded. This avoids consuming the first Delete merely to send stale WorkDrag::Cancel. `diagram/work_drag.rs` and app/allocation_drag.rs are untouched.
- Model confirmation is captured through existing model_menu::Opened plus selected identity/placement and a unique token. Rendered text uses the captured object. ConfirmDelete now requires the token. Cancel/Escape clear the snapshot and advance the canvas epoch. Old tokens cannot confirm a later dialog; changed document/revision/epoch/page/layer/diagram/object selection or placement refuses the command. Existing pointer/model menu deletion still invokes apply as before, then creates the captured confirmation.
- Existing architecture/state menus gain scope-appropriate Delete/Shift+Delete hints. Diagram background and unsupported state-action rows do not claim a removal shortcut.
## Changed files
New:
- `crates/oa-desktop/src/app/diagram_removal.rs` — intent validation, confirmation lifetime, shared keyboard admission predicate.
- `crates/oa-desktop/src/diagram/focus.rs` — actual Iced Focusable wrapper.
- `crates/oa-desktop/src/app/diagram_removal_tests.rs` — 10 mounted App-tree test functions.
Modified:
- `crates/oa-desktop/src/app.rs` — module/fields/messages, tokenized modal handling and focused delegation hunks.
- `crates/oa-desktop/src/app/behavior_lifecycle_tests.rs` — existing ConfirmDelete constructor updated to token signature.
- `crates/oa-desktop/src/app/state_diagram.rs` — delegates the captured removal event.
- `crates/oa-desktop/src/behavior/state_canvas.rs` — admitted Delete events; removes obsolete private focus boolean.
- `crates/oa-desktop/src/behavior/state_edit.rs` — captured removal event/context.
- `crates/oa-desktop/src/behavior/state_menu.rs` — shortcut hints.
- `crates/oa-desktop/src/behavior/state_view.rs` — focused wrapper.
- `crates/oa-desktop/src/diagram.rs` — admitted Delete, bend/gesture priority, retired work-press cleanup; removes obsolete private focus boolean.
- `crates/oa-desktop/src/model_menu.rs` — shortcut hints.
- `crates/oa-desktop/src/views.rs` — captured confirmation text/token and architecture canvas wrapper only. Allocation toolbar region untouched.
## Authored verification, not executed
Reuses production `behavior::canvas_workspace::tests::Mounted`, real App::view/UserInterface/renderer, retained widget cache, ordered input batches and task/widget-operation draining. Tests never seed canvas private gesture or focus booleans.
1. OA/SA real pointer selection → Delete exact placement; other diagram placement and semantic object retained; source/model agreement and complete one-unit Undo/Redo.
2. Shift+Delete captured target; Cancel, Escape, old-token replay versus a new dialog; intended object removal and Undo source round trip.
3. CLEAN Inspector focus by actual widget operation and canvas Tab; Tab+Delete in one native batch cannot remove model; Shift+Delete cannot reach canvas; Home/Delete edits Name while project/source remain accepted and unchanged.
4. Captured emitted removal intent replay after selection, epoch, owner diagram, document or revision change.
5. Window Unfocused, menu open/close, dirty draft, modal, epoch and unsupported modifier refusal, followed by explicit reacquisition.
6. Real route-pin selection → Delete only bend; source round trip; actual edge segment selection → confirmed semantic deletion.
7. Real state-canvas pointer selection → deletes state placement while architecture placement and state semantic object survive; Undo/source round trip.
8. Captured confirmation refuses changed selection/document/epoch/placement/revision.
9. Programmatic canvas focus without preceding mouse entry → Delete works through the single focus owner.
10. OA/SA Allocation work body click → first Delete without intervening redraw hides exact placement once; semantic work/performer preserved; Undo restores source.
Static checks performed: scoped rustfmt --edition 2024 --config skip_children=true on the 13 owned files; `git diff --check` passes. Formatter exit 0 is only a syntax-formatting check. No compilation or tests were run. Parent should run new `diagram_removal_tests` plus existing state/lifecycle, behavior lifecycle, model-menu, architecture and allocation full-tree tests on the combined integrated candidate.
## Explicit remaining gaps / review risks
- Process and scenario occurrence/link/fragment removal remains unimplemented; do not route their selections through architecture/state context or delete referenced reusable actors/functions. Scenario's modeled Delete participant event is untouched.
- Explorer keyboard removal remains unimplemented; existing Actions/pointer menu remains available.
- State action labels (state-action relation identities represented outside projected edges) remain pointer-menu deletion only. No shortcut hint is claimed for them.
- Mounted tests have not executed. Inspector Tab order, route-segment geometry, state selection hit geometry and the allocation no-redraw case are particularly important concrete checks. No screenshot/native UI acceptance has been done.
- Added tests exercise generated modal state and dispatch actual modal action messages through App; they do not locate/click the rendered confirmation buttons or read rendered glyph text. Parent native QA should confirm target wording and Cancel/Confirm accessibility.
- Tool/gesture admission guards are authored, but dedicated new mounted drag/connection/creation/reconnect and stale-invalid-bend cases are not yet authored. Existing relevant regression tests should be retained and broadened if they expose missing coverage.
- Intent validation compares current selection plus document/context lifetime. Switching selection away and back without any context/revision change is not recorded by a distinct selection-generation counter. Immediate changed-selection replay is covered. Parent should judge whether a stronger input-generation token is required for issue acceptance.
- `delete_pending` boolean remains for compatibility with existing guards/tests; production opening always also creates delete_target. Legacy tests that manually set only the boolean do not create a rendered target dialog, though App modal admission still blocks underlying action messages.
All candidate source files are frozen after this handoff. Further edits require parent follow-up.
@@ -0,0 +1,47 @@
# Allocation label checkpoint review — 2026-09-12
Reviewer: `/root/allocation_checkpoint_review`. Read-only source review of the saved `6b9cc77` allocation change, initially at `94b5eec`; the parent subsequently formatted it at `d9b3f0dc91e8882e6efc262c2e8d971998d2694e`. Read applicable AGENTS/PROJECT_GOAL, the v2 contract, prior repair review, planner, App, rendering and tests. Root owns remote/shared coordination. No compiler, tests, native UI, Gitea or Git/source mutations were performed; this report is the sole write. Findings are source-derived, not runtime observations.
## [P2] Joint-label refusal removes every destination point for an ordinary collapsed-performer drag
Primary location: `crates/oa-desktop/src/allocation_drag.rs:593` (new rejection at 597–602). Related: `app/allocation_drag.rs:98` (label geometry), `:120` (collapsed hit-region height), `:188` (frozen resolver), `:554` (pointer-relative requested placement).
Concrete OA/SA counterexample using `app/allocation_drag_tests.rs:28` fixture:
1. Rename A to `Receiving subsystem` (19 characters), and set its existing placement's `collapsed` flag. Keep A at `(100,100)` with default `224×94` size, Work at `(100,500)` with the same default size, and zoom 1/zero pan. No ports or other content are needed. The default size is defined at `oa-core/src/model.rs:1944`; neither the collapsed projection nor port minimum-size logic expands the width for this name (`projection.rs:314`, `projection/port_layout.rs:131`).
2. Press the work name at `(140,535)`, the normal fixture grab offset `(40,35)` (`app/allocation_drag_tests.rs:95`). Attempt to move/release anywhere in A's entire exposed header.
3. A's frozen targetable rectangle is half-open `[100,324) × [100,162)`. Its frozen label is `[110,308) × [131,153)` (19 characters × 10 + 8 = 198 wide).
4. For **every** pointer in that targetable rectangle, requested work coordinates are `(pointer.x−40,pointer.y−35)`. Grid snapping gives work-header left in `{60,70,…,280}` and top in `{70,80,…,130}`. The header remains `224×62`: the work is a leaf and only its new performer ancestor expands (`oa-core/src/projection.rs:541`).
5. Every such header has left < 308 and right ≥ 284 > 110, plus top ≤ 130 < 153 and bottom ≥ 132 > 131. Thus every position has positive-area overlap with the label and the new check refuses it. Moving below the label as instructed leaves the collapsed hit rectangle; this resolves to OutsidePerformers and cannot allocate/unfold A. Moving beside cannot clear the label while the pointer remains in A for this grab offset.
The attempted assignment is otherwise semantically valid and the candidate can unfold A (`allocation_drag.rs:557`). This makes successful ordinary collapsed-target assignment depend on knowing to restart with a different grab offset or manually expand the target. It contradicts the adopted collapsed-header workflow and makes the refusal's “Aim below or beside” recovery impossible within this held gesture. It is a regression caused by composing the new readability gate with existing frozen target geometry, not a request to weaken collision safety.
The existing collapsed-target resolver test (`app/allocation_drag_tests.rs:758`) checks only the header hit boundary. Planner success tests (`allocation_drag/tests.rs:986`, `:1491`) directly supply a destination and requested position; they do not prove an actual pointer inside a collapsed target can produce that position. They therefore miss this counterexample.
## What the saved fix does establish
- The original `Receiver` counterexample is rejected: selected projected header `(120,110)224×62` intersects the actual frozen `(110,131)88×22` label. Preview and release share `place`, and `finish` repeats it (`allocation_drag.rs:633`). Failure occurs before source execution (`app/allocation_drag.rs:563` onward). Existing model/source/history should consequently remain untouched on this path.
- The new check distinguishes label occlusion from legitimate performer enclosure; it does not remove the ancestor/descendant exemption from general card collision checks. Nearby ordinary expanded-target placement `(120,160)` remains valid by the inspected geometry.
- Existing packet test destinations changed from y140 to y190 while their semantic, identity, source and history assertions remain. This repairs formerly unreadable test inputs; it does not weaken those assertions. The new overlap test retains the old input as an explicit refusal and adds transformed OA/SA, preserved redo, exact nearby placement, semantic-cache reuse and one-step commit/Undo/Redo coverage (`app/allocation_drag_tests.rs:958`).
- The same-performer test preserves a readable snapped no-op and allocation identity (`allocation_drag/tests.rs:1115`). It does not independently cover same-performer occlusion, although that path shares the new check.
## Remedies compared against the actual workflow
The specification should distinguish **frozen hit geometry** from **readable destination identity presentation**. The user's requirements need clear nesting, readable work/destination and exact preview; they do not require a duplicate opaque target name to stay at its old world coordinates. Resolve the source contract before encoding further refusals.
| Candidate | Ordinary drag / frozen target / exact placement / readability | Tradeoff |
| --- | --- | --- |
| **Recommended: retain the frozen target outline; present its identity in a dedicated, reliably visible toolbar/status slot outside the canvas; remove its opaque duplicate name overlay.** | Preserves all destination hit rules and pointer placement, including collapsed targets; removes the overlay collision without moving work or target. | Amend the contract's frozen-label-in-header requirement and corresponding planner context. The identity slot must remain visible at narrow widths and with long names/refusal text; do not rely on arbitrary clipped status prose to preserve the name. Existing toolbar infrastructure at `views.rs:543` provides a starting point. |
| **Bounded alternative proposed by root: use the frozen badge if readable; otherwise use the same target's candidate projected header.** | Preserves frozen hit resolution, exact pointer placement and model geometry. Solves both the Receiver and collapsed long-name counterexamples at the fixture camera. | Store exact chosen badge bounds/text in PlacementPlan and consume those bounds in both admission and renderer; change the old requirement that the badge remain within the frozen accepted header. A toolbar fallback is still needed to guarantee reachability when the candidate header is outside the viewport. |
| Keep an in-canvas name badge but place it in a collision-free viewport location, separately from the frozen hit outline. | Can preserve frozen target resolution and exact work placement while keeping both labels readable. | Requires a deterministic badge-placement policy, explicit relation to its target, and fallback when no space exists. More geometry and crowded-view cases than the toolbar approach; changing badge position must never change target resolution. |
| Offset/pack the work below the frozen label, expand the hit zone, or merely ask the user to expand/regrab. | Packing breaks exact pointer placement; enlarging hidden target area breaks visible frozen resolution; expand/regrab leaves this ordinary valid gesture unsupported. | Reject as the default remedy for this finding. These alter the interaction contract or transfer the failure to users rather than resolving the presentation conflict. |
The candidate-header alternative has a useful source-level invariant: for a direct performer ancestor, candidate target top is at most work top minus 64 (`projection.rs:568`). A badge at target top + 31 with height 22 therefore ends at least 11 units above the work header. The Receiver root y110 gives candidate badge y77–99; the collapsed example roots y70–130 give badge tops y37–97. Both clear the selected header. Indirect ancestors through coherently projected work parents retain additional header separation. Keep the frozen name/size or explicitly derive and store any changed truncation so geometry and actual text do not diverge.
Its unresolved viewport case is concrete: use the same collapsed long-name fixture with pan y−100 so the target starts at the canvas top (world viewport top=100). Every reachable header still intersects the frozen label. Only work roots y100–130 satisfy selected-header viewport containment, while candidate badge tops are y67–97, all above the viewport. Frozen-first/candidate-second therefore still admits no point in the collapsed target for that grab. A named viewport refusal would be truthful, but would retain the reachability limitation; a toolbar destination fallback avoids it without moving the camera or work. This should be an explicit decision and regression, rather than silently claiming the candidate-header fallback solves all valid collapsed drops.
## Regression evidence still needed
A full-tree regression is warranted because the defect is a composition of pointer admission, frozen targets, projection and a separately stacked feedback canvas (`views.rs:462`). Exercise OA and SA with the long-name collapsed target above and the ordinary `(40,35)` grab; require a successful exact-placement assignment/unfold, readable selected work and destination identity, and single-step Undo/Redo. Pair it with the original Receiver geometry and transformed camera. Assert the actual visible presentation appropriate to the chosen remedy, not just planner acceptance or unchanged pixels when unrelated toolbar text varies.
The current full-tree pixel test (`app/allocation_drag_full_tree_tests.rs:252`) proves toolbar isolation by changing only feedback prose. It cannot establish joint label readability or collapsed-target reachability. Minimum-window layout and packaged native acceptance remain root-owned gates; this review alone does not establish them.
@@ -0,0 +1,25 @@
{
"source": "d9b3f0dc91e8882e6efc262c2e8d971998d2694e",
"files": [
{
"path": "full-d9b3f0d.log",
"bytes": 90091,
"sha256": "368c9d12327a6c5df0c8aab3db4fc950a0b6045348bf47ac5aaba5739dd02d00"
},
{
"path": "full-d9b3f0d.inputs.json",
"bytes": 98013,
"sha256": "5183c5000c41bd392733b111131e3b2f6da084bc58e92002cdf5b14e29512f77"
},
{
"path": "full-d9b3f0d.result.json",
"bytes": 385,
"sha256": "92a5dc936a1920da3c787f66c7b1c80e29a85d19a68c64df5528d3ba29205782"
},
{
"path": "allocation-checkpoint-review-20260912.md",
"bytes": 10048,
"sha256": "bcc1a368283ba2e1f9dc798f14f71ffbe530daf27ec88953594a39afa0f8b543"
}
]
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,800 @@
Compiling oa-desktop v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\crates\oa-desktop)
Finished `test` profile [unoptimized] target(s) in 2m 50s
Running unittests src\main.rs (target\debug\deps\arcadia_studio-e358a5ed59b1de41.exe)
running 792 tests
test allocation_drag::tests::allocation_artifact_and_manual_trace_cascades_are_named_and_refused ... ok
test allocation_drag::tests::cache_plan_uses_candidate_index_and_same_pixel_different_target_changes_preview ... ok
test allocation_drag::tests::collapsed_target_unfold_is_explicit_and_no_other_fold_changes ... ok
test allocation_drag::tests::direct_exit_revealing_another_role_performer_has_truthful_refusal ... ok
test allocation_drag::tests::explicit_different_performer_retains_advisory_role_warning ... ok
test allocation_drag::tests::folded_subtree_cannot_hide_new_collision ... ok
test allocation_drag::tests::frozen_label_readability_allows_touching_edges_but_refuses_positive_area_overlap ... ok
test allocation_drag::tests::frozen_performer_label_refuses_overlapping_work_header_and_allows_nearby_exact_move ... ok
test allocation_drag::tests::geometry_refuses_new_overlap_bounds_and_invisible_headers_and_preserves_existing_overlap ... ok
test allocation_drag::tests::malformed_scope_wrong_projection_duplicate_placement_and_stable_id_collision_refuse ... ok
test allocation_drag::tests::mixed_p_x_y_refuses_semantic_and_arrange_instead_of_stretching_destination ... ok
test allocation_drag::tests::oa_communication_incompleteness_remains_advisory_without_collateral_loss ... ok
test allocation_drag::tests::oa_sa_incoming_between_outgoing_preserve_identity_source_history_and_other_views ... ok
test allocation_drag::tests::oa_sa_one_leaf_move_refuses_when_ancestor_regrouping_introduces_unrelated_overlap ... ok
test allocation_drag::tests::oa_sa_one_leaf_reassignment_and_outward_move_regroup_ancestors_without_moving_sibling ... ok
test allocation_drag::tests::oa_sa_populated_collapsed_target_accepts_room_and_refuses_collision_without_relocating_contents ... ok
test allocation_drag::tests::oa_sa_state_owned_action_and_availability_reject_reassignment_atomically ... ok
test allocation_drag::tests::oa_sa_unfolding_prior_hidden_overlap_refuses_even_when_insertion_does_not_collide ... ok
test allocation_drag::tests::own_deleted_allocation_routes_may_disappear_in_every_diagram_only ... ok
test allocation_drag::tests::role_only_same_effective_performer_is_semantic_noop_and_outgoing_refuses ... ok
test allocation_drag::tests::sa_port_and_deferred_exchange_allocation_cascade_refuses_and_retarget_is_invalid ... ok
test allocation_drag::tests::same_performer_readability_preserves_snapped_noop_and_allocation_identity ... ok
test allocation_drag::tests::scope_detects_first_child_allocation_and_role_transfer_and_finish_rejects_live_edits ... ok
test allocation_drag::tests::snapped_noop_returns_no_command_and_keeps_source_revision_and_redo ... ok
test allocation_drag::tests::source_budget_refusal_keeps_project_bundle_history_and_retry_identity_exact ... ok
test allocation_drag::tests::surviving_constraint_mutation_is_refused_even_when_core_candidate_is_valid ... ok
test allocation_drag::tests::target_presentation_must_name_the_destination_and_its_visible_frozen_header ... ok
test allocation_drag::tests::uniform_folded_grandchild_and_unplaced_scope_moves_exactly_q_and_all_l_oa_sa ... ok
test allocation_drag::tests::uniform_role_only_and_mixed_direct_role_composites_same_target_do_not_allocate ... ok
test allocation_drag::tests::uniformly_unassigned_composite_creates_only_stable_leaf_allocations_including_unplaced ... ok
test allocation_drag::tests::unrequested_view_option_cleanup_is_not_whitelisted ... ok
test allocation_drag::tests::unresolved_role_leaf_can_arrange_or_accept_explicit_target_without_guessing ... ok
test app::allocation_drag::tests::actual_canvas_origin_change_cancels_held_work_before_any_legacy_release ... ok
test app::allocation_drag::tests::arrange_is_one_gesture_and_snapped_noop_preserves_redo_and_source ... ok
test app::allocation_drag::tests::candidate_semantics_are_cached_per_destination_with_stable_ids_and_own_index ... ok
test app::allocation_drag::tests::frozen_collapsed_target_uses_only_visible_header_and_port_edge_controls_occlude ... ok
test app::allocation_drag::tests::frozen_targets_use_half_open_semantic_priority_and_refuse_incomparable_overlap ... ok
test app::allocation_drag::tests::full_tree::actual_app_tree_dirty_description_retains_focus_and_middle_caret_for_next_key ... ok
test app::allocation_drag::tests::full_tree::actual_app_tree_dirty_name_retains_focus_and_middle_caret_for_next_key ... ok
test app::allocation_drag::tests::full_tree::actual_keyboard_arrange_shortcut_consumes_once_escape_cancels_and_dirty_owner_refuses ... ok
test app::allocation_drag::tests::full_tree::actual_rendered_canvas_pixels_do_not_change_when_toolbar_feedback_text_changes ... ok
test app::allocation_drag::tests::modal_roundtrip_equal_fit_undo_resize_and_focus_retire_press_before_replay ... ok
test app::allocation_drag::tests::mounted_clean_stationary_selects_exact_work_without_revision_or_legacy_apply ... ok
test app::allocation_drag::tests::mounted_dirty_port_press_cannot_fall_through_to_applying_port_selection ... ok
test app::allocation_drag::tests::mounted_dirty_property_press_does_not_select_apply_or_destroy_draft ... ok
test app::allocation_drag::tests::mounted_oa_sa_ordered_incoming_between_outgoing_are_exact_single_history_steps ... ok
test app::allocation_drag::tests::mounted_role_only_outgoing_refusal_retains_placement_and_same_performer_creates_no_allocation ... ok
test app::allocation_drag::tests::mounted_uniform_parent_moves_hidden_placements_and_assigns_only_leaves ... ok
test app::allocation_drag::tests::oa_sa_label_overlap_refuses_preview_release_and_retries_without_partial_source_or_history ... ok
test app::allocation_drag::tests::outside_release_is_cancel_and_does_not_remove_an_assignment ... ok
test app::allocation_drag::tests::pending_behavior_draft_refuses_actual_work_press_and_equal_document_replacement_retires_packet ... ok
test app::allocation_drag::tests::release_uses_ordered_native_position_before_later_motion_in_same_batch ... ok
test app::allocation_drag::tests::same_snapped_position_on_opposite_shared_border_rebuilds_semantics_even_if_layout_refuses ... ok
test app::allocation_drag::tests::source_and_modal_admission_reject_prebuilt_clean_press_without_selecting ... ok
test app::allocation_drag::tests::source_refusal_is_atomic_and_a_new_gesture_can_retry_against_live_revision ... ok
test app::allocation_drag::tests::stale_packets_do_not_cancel_a_newer_token_and_each_release_is_consumed_once ... ok
test app::allocation_drag::tests::toolbar_status_leaves_transformed_top_edge_frozen_target_label_readable ... ok
test app::allocation_drag::tests::toolbar_status_leaves_transformed_top_edge_work_header_readable ... ok
test app::architecture_authoring::tests::architecture_allocated_work_is_root_owned_and_one_exact_history_change ... ok
test app::architecture_authoring::tests::architecture_blank_names_and_escape_never_accept_placeholder_or_rename ... ok
test app::architecture_authoring::tests::architecture_captured_escape_does_not_cancel_other_controls_or_placement ... ok
test app::architecture_authoring::tests::architecture_f2_routes_selected_node_to_inline_rename_and_exact_undo ... ok
test app::architecture_authoring::tests::architecture_first_focused_input_escape_cancels_create_and_rename ... ok
test app::architecture_authoring::tests::architecture_invalid_click_cannot_poison_inline_or_checkpoint_geometry ... ok
test app::architecture_authoring::tests::architecture_stale_submit_keeps_create_and_rename_visible_and_exact ... ok
test app::architecture_authoring_tests::architecture_cancelled_gesture_cannot_commit_a_later_named_draft ... ok
test app::architecture_authoring_tests::architecture_canvas_left_click_uses_world_coordinate_once_at_zoom_and_pan ... ok
test app::architecture_authoring_tests::architecture_checkpoint_retains_exact_name_point_and_fresh_binding_without_commands ... ok
test app::architecture_authoring_tests::architecture_context_choices_are_small_and_system_is_singleton ... ok
test app::architecture_authoring_tests::architecture_contextual_allocation_creates_work_relation_and_node_in_one_undo ... ok
test app::architecture_authoring_tests::architecture_creation_names_before_commit_preserves_camera_and_undoes_once ... ok
test app::architecture_authoring_tests::architecture_existing_inventory_is_contextual_and_placement_never_implies_allocation ... ok
test app::architecture_authoring_tests::architecture_f2_rename_preserves_identity_placement_and_atomic_history ... ok
test app::architecture_authoring_tests::architecture_name_survives_rejected_click_resize_navigation_and_escape_is_explicit ... ok
test app::architecture_authoring_tests::architecture_rejected_submission_keeps_inline_name_visible_and_invalid_click_is_ignored ... ok
test app::architecture_authoring_tests::basic_transition_explicit_overview_focus_wins_over_an_already_populated_architecture ... ok
test app::behavior_lifecycle_tests::incomplete_behavior_form_is_dirty_close_save_retains_it_and_cancel_allows_switching ... ok
test app::behavior_lifecycle_tests::passive_behavior_canvas_events_do_not_apply_property_drafts ... ok
test app::behavior_lifecycle_tests::pending_behavior_blocks_navigation_property_edits_and_model_history ... ok
test app::behavior_lifecycle_tests::pending_behavior_blocks_source_creation_and_source_history_replay ... ok
test app::behavior_lifecycle_tests::save_prepares_behavior_before_opening_destination_and_cancel_keeps_one_undo ... ok
test app::behavior_lifecycle_tests::source_writeback_failure_keeps_typed_behavior_form_for_repair_and_one_undo ... ok
test app::checkpoints::tests::checkpoint_completion_releases_the_writer_while_a_context_menu_is_open ... ok
test app::checkpoints::tests::checkpoint_inline_create_and_rename_restore_focus_without_changing_drafts_or_history ... ok
test app::checkpoints::tests::checkpoint_noninline_contexts_keep_other_focus_and_still_lint ... ok
test app::checkpoints::tests::discard_and_close_waits_for_the_late_checkpoint_then_retires_it ... ok
test app::checkpoints::tests::home_resume_keeps_state_view_while_explicit_architecture_remains_reachable ... ok
test app::checkpoints::tests::home_resume_retains_invalid_source_and_property_drafts ... ok
test app::checkpoints::tests::home_resume_returns_to_each_actual_workspace_without_model_edits ... ok
test app::checkpoints::tests::malformed_checkpoint_does_not_replace_the_current_model ... ok
test app::checkpoints::tests::recent_and_recovery_navigation_respect_unsaved_decision ... ok
test app::checkpoints::tests::restored_copy_keeps_raw_property_and_requires_save_without_local_or_hosted_binding ... ok
test app::checkpoints::tests::saving_an_older_snapshot_keeps_recovery_for_newer_unsaved_work ... ok
test app::checkpoints::tests::stale_write_completion_cannot_recover_a_discarded_document ... ok
test app::checkpoints::tests::walkthrough_request_keeps_unfinished_source_behind_the_normal_pending_guard ... ok
test app::data_paging_lifecycle_tests::paging_preserves_same_context_and_resets_on_layer_or_document_replacement ... ok
test app::diagram_selection_tests::changing_visible_view_follows_its_clean_inspector_without_a_model_edit ... ok
test app::diagram_selection_tests::diagram_switch_preserves_an_independent_semantic_inspection ... ok
test app::diagram_selection_tests::diagram_uuid_edit_follows_the_same_view_or_a_different_picker_target ... ok
test app::diagram_selection_tests::invalid_view_draft_blocks_the_switch_and_retains_raw_input ... ok
test app::diagram_selection_tests::stale_diagram_choice_never_applies_an_unrelated_valid_draft ... ok
test app::diagram_selection_tests::valid_pending_view_edit_applies_to_old_view_then_inspects_the_new_view ... ok
test app::direct_scenario_tests::direct_scenario_camera_details_and_real_resize_invalidate_only_pointer_context ... ok
test app::direct_scenario_tests::direct_scenario_creation_and_reorder_preserve_selection_and_each_undo_once ... ok
test app::direct_scenario_tests::direct_scenario_rejects_every_stale_context_and_preserves_property_drafts ... ok
test app::direct_scenario_tests::direct_scenario_source_failure_and_navigation_keep_form_until_success ... ok
test app::hosted::tests::hosted_actual_app_tasks_publish_edit_conflict_correct_credentials_reload_and_open ... ok
test app::hosted::tests::hosted_catalog_invalidation_follows_service_writes_and_observed_revision_changes ... ok
test app::hosted::tests::hosted_catalog_search_credentials_and_failed_refresh_never_mix_result_scopes ... ok
test app::hosted::tests::hosted_changed_bound_head_does_not_invalidate_another_services_empty_filter ... ok
test app::hosted::tests::hosted_conflict_fetches_review_without_adoption_and_reload_is_undoable ... ok
test app::hosted::tests::hosted_detached_drafts_and_recovery_remain_available_after_rejected_publish ... ok
test app::hosted::tests::hosted_failed_cross_service_publication_checks_and_reloads_its_own_resource ... ok
test app::hosted::tests::hosted_failed_or_cancelled_local_open_keeps_the_existing_document_and_destination ... ok
test app::hosted::tests::hosted_invalid_and_detached_source_drafts_never_start_network_writes ... ok
test app::hosted::tests::hosted_invalid_local_open_repair_saves_locally_and_never_replaces_old_resource ... ok
test app::hosted::tests::hosted_modal_blocks_underlying_edits_and_credentials_are_redacted ... ok
test app::hosted::tests::hosted_new_revision_invalidates_filtered_catalog_even_without_a_displayed_row ... ok
test app::hosted::tests::hosted_only_successful_source_save_as_changes_the_active_destination ... ok
test app::hosted::tests::hosted_open_waits_for_unsaved_guard_and_failed_open_preserves_current_document ... ok
test app::hosted::tests::hosted_publish_acknowledges_only_the_sent_snapshot_and_retains_undo ... ok
test app::hosted::tests::hosted_stale_completions_do_not_clear_current_busy_or_adopt_another_document ... ok
test app::hosted::tests::hosted_unknown_save_is_acknowledged_by_exact_readback_without_retry ... ok
test app::lint_tests::committed_check_cache_follows_edit_and_undo_while_drafts_stay_separate ... ok
test app::lint_tests::lint_coalesces_work_and_rejects_obsolete_results ... ok
test app::lint_tests::prior_document_lint_cannot_enter_reopened_model_or_block_next_checks ... ok
test app::port_view_tests::catalog_creation_follows_the_new_port_and_keeps_group_browsing_explicit ... ok
test app::port_view_tests::filtered_active_view_cannot_hide_a_created_port_when_eligible_owner_view_is_folded ... ok
test app::process_canvas::direct_process_dirty_source_blocks_canvas_creation_without_losing_text ... ok
test app::process_canvas::tests::direct_process_app_open_source_and_back_preserve_local_reference_identity ... ok
test app::process_canvas::tests::direct_process_app_same_owner_selection_preserves_tool_but_owner_change_resets_it ... ok
test app::process_canvas::tests::direct_process_app_save_details_resize_and_passive_geometry_keep_drafts_and_camera ... ok
test app::process_canvas::tests::direct_process_app_source_failure_retains_form_then_commits_once_and_selects_occurrence ... ok
test app::process_canvas::tests::direct_process_app_stale_context_dirty_source_and_properties_never_mutate ... ok
test app::process_canvas::tests::direct_process_append_empty_first_second_exchange_has_exact_source_history ... ok
test app::process_canvas::tests::process_focus_retains_rejected_draft_and_guards_selection_until_panels_restore ... ok
test app::reference_authoring::tests::chooser_rejects_old_results_after_context_recovery_reload_or_modal_and_keeps_history_text_out ... ok
test app::reference_authoring::tests::inspector_reference_browse_is_read_only_then_one_scoped_value_applies_with_exact_undo_redo ... ok
test app::reference_authoring::tests::inspector_reference_retains_invalid_missing_and_incompatible_current_until_explicit_clear ... ok
test app::reference_authoring::tests::mounted::mounted_delayed_return_cannot_focus_under_review_and_fresh_modal_tab_owns_its_form ... ok
test app::reference_authoring::tests::mounted::mounted_form_tab_enters_in_both_directions_and_cycles_boundaries_without_losing_focus ... ok
test app::reference_authoring::tests::mounted::mounted_fresh_tab_reveals_offscreen_reference_and_return_reveals_it_again ... ok
test app::reference_authoring::tests::mounted::mounted_reference_choice_stages_literal_identity_and_delayed_focus_cannot_hit_replacement_field ... ok
test app::reference_authoring::tests::mounted::mounted_reference_trigger_tabs_to_root_popup_search_escape_and_exact_adjacent_fields ... ok
test app::reference_authoring::tests::mounted::mounted_reverse_review_required_rationale_remains_editable_beside_candidate_reference_form ... ok
test app::reference_authoring::tests::mounted::mounted_review_pointer_open_and_outside_dismissal_keep_background_input_inactive ... ok
test app::reference_authoring::tests::mounted::mounted_scrolled_reference_anchor_uses_window_geometry_for_pointer_and_keyboard ... ok
test app::reference_authoring::tests::reference_array_row_reorder_and_constructor_switch_invalidate_exact_old_field ... ok
test app::reference_authoring::tests::review_reference_cannot_reopen_or_write_a_banked_row_after_another_row_takes_focus ... ok
test app::reference_authoring::tests::review_reference_facets_groups_and_attachment_hosts_browse_without_replanning_and_reject_old_facet ... ok
test app::repair_save_state_tests::recovering_applied_repair_rebinds_rolled_back_disk_bytes_without_changing_model ... ok
test app::repair_save_state_tests::recovery_missing_entry_or_failed_reload_preserves_current_destination_save_authority ... ok
test app::repair_save_state_tests::repair_history_compares_to_new_disk_baseline_and_then_successful_save ... ok
test app::repair_save_state_tests::repair_to_previous_saved_bytes_requires_save_at_new_destination_and_guards_close ... ok
test app::repair_save_state_tests::valid_equal_content_at_distinct_destinations_stays_clean ... ok
test app::review_navigation_tests::rejected_semantic_source_writeback_retains_review_camera_and_original_navigation ... ok
test app::review_navigation_tests::review_already_showing_the_target_keeps_its_camera_and_query ... ok
test app::review_navigation_tests::semantic_name_take_selects_the_counterpart_view_in_both_directions_with_one_exact_history_entry ... ok
test app::review_navigation_tests::semantic_review_without_a_destination_view_clears_the_old_layer_canvas_without_creating_a_view ... ok
test app::save_as_tests::save_as_late_receipt_preserves_newer_raw_source_draft_and_destination ... ok
test app::save_as_tests::save_as_preflight_rejects_historical_collision_before_starting_io ... ok
test app::save_as_tests::save_as_receipt_then_one_undo_restores_meaning_and_saving_preserves_redo ... ok
test app::source_project_tests::closed_detached_file_keeps_first_save_conflict_then_can_be_reloaded_fresh ... ok
test app::source_project_tests::inactive_invalid_draft_blocks_graphical_changes_and_diagnostic_switches_files ... ok
test app::source_project_tests::json_export_and_rejected_save_as_preserve_source_location_history_and_bytes ... ok
test app::source_project_tests::missing_include_open_keeps_old_model_and_can_be_repaired_and_saved ... ok
test app::source_project_tests::recovery_after_first_save_failure_retains_target_then_retries_without_reentering_path ... ok
test app::source_project_tests::recovery_of_previous_save_as_target_does_not_redirect_current_project ... ok
test app::source_project_tests::recovery_open_finds_first_save_journal_without_an_entry_file ... ok
test app::source_project_tests::recovery_target_survives_edit_lint_apply_and_rejected_recovery_until_success ... ok
test app::source_project_tests::source3_native_open_preserves_report_comments_nesting_and_save_reopen ... ok
test app::source_project_tests::two_file_apply_graphical_edit_exact_undo_save_as_and_reopen ... ok
test app::state_diagram_tests::all_context_dimensions_reject_stale_selection_camera_and_model_messages ... ok
test app::state_diagram_tests::create_view_places_only_its_machine_and_restores_exact_source_in_one_undo ... ok
test app::state_diagram_tests::direct_create_connect_select_and_move_keep_semantic_identity_and_atomic_history ... ok
test app::state_diagram_tests::fallback_owner_first_selection_keeps_the_captured_context_for_its_first_move ... ok
test app::state_diagram_tests::pending_drafts_block_every_direct_mutation_and_passive_camera_preserves_inputs ... ok
test app::state_diagram_tests::real_view_tool_and_history_changes_invalidate_old_release_events ... ok
test app::state_diagram_tests::rejected_creation_keeps_source_and_selection_then_valid_owner_can_retry ... ok
test app::state_diagram_tests::same_size_viewport_after_revision_or_tool_epoch_does_not_refit_the_camera ... ok
test app::state_diagram_tests::shared_semantic_states_keep_architecture_and_behavior_placements_independent ... ok
test app::state_diagram_tests::state_diagram_routes_constructor_and_source_open_to_the_existing_workspace_without_edits ... ok
test app::state_diagram_tests::state_diagram_routes_picker_and_reveal_but_explicit_architecture_does_not_bounce ... ok
test app::state_diagram_tests::state_diagram_routes_preserve_selected_raw_properties_and_pending_behavior_guards ... ok
test app::state_diagram_tests::state_navigation_catalog_selection_across_layers_resets_camera_and_tool ... ok
test app::state_diagram_tests::state_navigation_catalog_selection_of_another_machine_resets_camera_and_tool ... ok
test app::state_diagram_tests::state_navigation_layer_change_resets_the_new_machine_at_the_same_viewport_size ... ok
test app::state_diagram_tests::state_navigation_return_to_the_same_view_preserves_camera_tool_and_fit_mode ... ok
test app::state_diagram_tests::state_navigation_root_event_and_same_layer_keep_the_current_camera_and_tool ... ok
test app::transition_context_tests::explicit_behavior_internal_and_process_roots_keep_their_workspace_without_creating_a_view ... ok
test app::transition_context_tests::explicit_destination_is_honored_and_ordinary_work_transition_still_places_in_architecture ... ok
test app::transition_context_tests::machine_and_typed_action_transitions_resolve_the_owner_and_reusing_that_context_retains_camera ... ok
test app::transition_context_tests::rejected_source_writeback_keeps_review_and_navigation_and_invalid_draft_still_blocks_start ... ok
test app::transition_context_tests::scenario_transition_focuses_requested_counterpart_and_preserves_unrelated_views_in_both_directions ... ok
test app::view_copy_tests::copying_architecture_keeps_architecture_even_with_a_retained_state_view ... ok
test app::view_copy_tests::copying_the_current_state_view_uses_its_own_identity_and_is_one_source_undo ... ok
test app::view_copy_tests::duplicate_rejection_preserves_an_incomplete_source_buffer ... ok
test app::view_copy_tests::duplicate_rejection_preserves_invalid_property_and_pending_behavior_drafts ... ok
test app::view_copy_tests::stale_copy_does_not_apply_an_unrelated_valid_property_draft ... ok
test behavior::canvas_workspace::tests::relative_camera_zoom_keeps_the_cursor_world_point_and_rejects_nonfinite_inputs ... ok
test behavior::canvas_workspace::tests::viewport_ordered_batch_uses_each_event_position_and_cancels_before_camera_motion ... ok
test behavior::contextual_source_tests::contextual_source_app_checkpoint_restores_inner_raw_draft_without_model_adoption ... ok
test behavior::contextual_source_tests::contextual_source_app_empty_actions_and_child_open_refuse_other_raw_editors ... ok
test behavior::contextual_source_tests::contextual_source_app_modal_escape_then_child_back_then_parent_cancel_preserve_accepted_source ... ok
test behavior::contextual_source_tests::contextual_source_back_remains_available_after_owner_deletion_and_with_dirty_editors ... ok
test behavior::contextual_source_tests::contextual_source_back_retains_parent_and_rejects_late_child_event ... ok
test behavior::contextual_source_tests::contextual_source_default_consumer_name_has_stable_alias_and_correlated_selection ... ok
test behavior::contextual_source_tests::contextual_source_deleted_owner_view_retains_draft_instead_of_panicking_or_using_fallback ... ok
test behavior::contextual_source_tests::contextual_source_empty_history_reports_no_change_and_success_requires_a_real_history_step ... ok
test behavior::contextual_source_tests::contextual_source_empty_palette_lifeline_cannot_open_a_previous_owner_or_layer ... ok
test behavior::contextual_source_tests::contextual_source_existing_choice_prunes_staged_dependency ... ok
test behavior::contextual_source_tests::contextual_source_failed_source_budget_preserves_both_drafts_and_accepted_bytes ... ok
test behavior::contextual_source_tests::contextual_source_incomplete_recovered_kind_reports_error_without_panicking ... ok
test behavior::contextual_source_tests::contextual_source_inserts_work_on_the_exact_guarded_link_and_refuses_exchange_identity_conflict ... ok
test behavior::contextual_source_tests::contextual_source_lifelines_stage_exact_separate_records_and_one_history_entry ... ok
test behavior::contextual_source_tests::contextual_source_machine_before_any_owner_creates_optional_view_atomically ... ok
test behavior::contextual_source_tests::contextual_source_mounted_small_editor_keeps_back_and_use_outside_field_scroll ... ok
test behavior::contextual_source_tests::contextual_source_nested_semantic_parent_is_explicit_and_revalidated ... ok
test behavior::contextual_source_tests::contextual_source_new_callee_lifeline_preserves_caller_bindings_as_an_explicit_warning ... ok
test behavior::contextual_source_tests::contextual_source_original_document_remains_bound_after_back_or_choosing_existing ... ok
test behavior::contextual_source_tests::contextual_source_owner_first_can_return_to_an_unnamed_machine_without_losing_source ... ok
test behavior::contextual_source_tests::contextual_source_recovery_keeps_inner_raw_fields_and_refuses_deleted_or_changed_owner ... ok
test behavior::contextual_source_tests::contextual_source_second_system_and_role_machine_owner_refuse_without_fallback ... ok
test behavior::contextual_source_tests::contextual_source_work_uses_reserved_contribution_and_link_ids_and_one_undo ... ok
test behavior::creation::app_tests::app_choosing_another_behavior_or_layer_cannot_commit_a_creation_draft ... ok
test behavior::creation::app_tests::app_creation_opens_the_actual_new_owner_instead_of_first_or_previously_active_behavior ... ok
test behavior::creation::app_tests::app_first_captured_escape_dismisses_file_or_pending_overlay_before_creation ... ok
test behavior::creation::app_tests::app_lesson_request_cancel_retains_creation_then_escape_discards_only_the_draft ... ok
test behavior::creation::app_tests::app_machine_behavior_submit_opens_its_opted_in_view_as_one_undoable_transaction ... ok
test behavior::creation::app_tests::app_new_never_auto_applies_valid_or_invalid_property_and_source_drafts ... ok
test behavior::creation::app_tests::app_recovered_rootless_sa_creation_retains_layer_ids_and_other_raw_drafts ... ok
test behavior::creation::app_tests::app_stale_layer_open_never_starts_or_replaces_a_draft ... ok
test behavior::creation::tests::all_four_viewpoints_are_explicit_and_survive_source_history_reopen ... ok
test behavior::creation::tests::cancel_and_illegal_family_layer_switches_never_create_model_objects ... ok
test behavior::creation::tests::corrupted_recovery_and_reserved_identity_collision_refuse_without_repairing_raw_values ... ok
test behavior::creation::tests::creation_selection_rejects_changed_scenario_body_and_post_creation_operations ... ok
test behavior::creation::tests::machine_requires_valid_owner_and_first_view_adds_no_region_or_state ... ok
test behavior::creation::tests::mounted_creation_keeps_actions_visible_and_renders_only_six_owner_candidates ... ok
test behavior::creation::tests::oa_process_and_sa_chain_keep_exact_names_root_ownership_and_one_history_entry ... ok
test behavior::creation::tests::optional_context_collapses_without_losing_authored_reference_or_raw_search ... ok
test behavior::creation::tests::owner_search_is_name_ordered_filtered_and_page_sized_not_a_catalog ... ok
test behavior::creation::tests::post_commit_selection_is_correlated_with_exact_created_behavior_and_view ... ok
test behavior::creation::tests::recovery_preserves_exact_raw_inputs_reserved_ids_and_revalidates_stale_owner ... ok
test behavior::creation::tests::rejected_source_transaction_keeps_draft_and_every_accepted_source_byte ... ok
test behavior::creation::tests::repeated_owner_names_remain_distinguishable_by_searchable_parent_context_and_alias ... ok
test behavior::direct_canvas_tests::direct_alt_execution_stops_each_operand_at_its_reply_and_marks_open_operand ... ok
test behavior::direct_canvas_tests::direct_bounds_change_during_held_drag_never_commits_old_slot ... ok
test behavior::direct_canvas_tests::direct_clicked_occurrence_capture_cancels_on_view_and_context_changes ... ok
test behavior::direct_canvas_tests::direct_context_escape_unfocus_and_pending_form_cancel_gestures ... ok
test behavior::direct_canvas_tests::direct_drag_and_click_click_use_ordered_sources_and_publish_once ... ok
test behavior::direct_canvas_tests::direct_execution_span_ends_at_its_correlated_reply_and_marks_an_open_call ... ok
test behavior::direct_canvas_tests::direct_panned_canvas_maps_ordered_pointer_events_to_the_visible_slot ... ok
test behavior::direct_canvas_tests::direct_scroll_resize_and_bounds_changes_cancel_captured_slots ... ok
test behavior::direct_canvas_tests::direct_selected_handle_reorders_and_hits_keep_semantic_identity ... ok
test behavior::direct_canvas_tests::reused_exchange_messages_display_their_distinct_occurrence_names ... ok
test behavior::scenario::data_contract_tests::large_payload_has_one_editor_with_reachable_field_and_authored_pages ... ok
test behavior::scenario::data_contract_tests::recursive_browsing_up_and_root_keep_values_and_report_the_depth_limit ... ok
test behavior::scenario::data_contract_tests::recursive_value_focus_exposes_authored_leaf_and_phase_changes_retain_surviving_keys ... ok
test behavior::scenario::data_contract_tests::reply_call_picker_previews_the_same_value_reset_as_selecting_another_exchange ... ok
test behavior::scenario::data_contract_tests::scenario_form_commits_two_reused_record_values_and_correlated_reply_atomically ... ok
test behavior::scenario::direct_tests::direct_busy_save_keeps_details_current_and_camera_for_the_next_pointer_gesture ... ok
test behavior::scenario::direct_tests::direct_clicked_occurrence_rejects_stale_slots_and_retains_recoverable_drafts ... ok
test behavior::scenario::direct_tests::direct_clicked_occurrences_use_root_and_operand_slots_with_exact_source_history ... ok
test behavior::scenario::direct_tests::direct_exchange_compatibility_browsing_preserves_final_slot_validation ... ok
test behavior::scenario::direct_tests::direct_exchange_picker_is_bounded_and_browsing_keeps_pending_values ... ok
test behavior::scenario::direct_tests::direct_five_hundred_exchange_choice_render_probe ... ok
test behavior::scenario::direct_tests::direct_form_keeps_the_same_canvas_in_the_widget_tree ... ok
test behavior::scenario::direct_tests::direct_middle_pair_insertion_is_one_exact_source_and_model_undo ... ok
test behavior::scenario::direct_tests::direct_operand_pair_keeps_parent_sequence_and_cannot_reorder_across_operands ... ok
test behavior::scenario::direct_tests::direct_reply_choices_cache_keeps_typing_fast_and_revalidates_changed_contexts ... ok
test behavior::scenario::direct_tests::direct_reply_choices_validate_the_actual_slot_and_reorder_rejects_reverse_order ... ok
test behavior::scenario::direct_tests::direct_scenario_reference_navigation_blocks_pending_drafts_and_stale_contexts ... ok
test behavior::scenario::direct_tests::direct_scenario_reference_navigation_retains_each_exact_caller_without_history ... ok
test behavior::scenario::direct_tests::direct_scenario_reference_return_survives_rename_and_rejects_deleted_or_changed_use ... ok
test behavior::scenario::operation_tests::operation_gui_async_picker_uses_request_root_and_nested_directions_without_erasing_rejected_values ... ok
test behavior::scenario::operation_tests::operation_gui_selects_two_parameterless_allocations_without_inventing_refinement_and_reply_derives_call ... ok
test behavior::scenario::operation_tests::operation_gui_unresolved_and_disabled_selection_recovery_preserves_rejected_raw_values ... ok
test behavior::scenario::tests::fragment_operands_accept_paired_messages_through_normal_destination_picker ... ok
test behavior::scenario::tests::graphical_call_reply_is_atomic_correlated_and_source_equivalent ... ok
test behavior::scenario::tests::invalid_lifecycle_edit_keeps_draft_and_timer_has_no_global_exchange ... ok
test behavior::scenario::tests::lifeline_picker_uses_core_viewpoint_contract ... ok
test behavior::scenario::tests::reverse_communication_canvas_choices_and_correlated_reply_share_core_direction ... ok
test behavior::scenario::tests::scenario_reuse_requires_explicit_compatible_bindings ... ok
test behavior::scenario::tests::tagged_scenario_commit_is_not_redirected_by_other_editor_events ... ok
test behavior::scenario::workspace::tests::new_scenario_form_resets_only_its_scroll_and_recovery_gets_a_fresh_session ... ok
test behavior::scenario::workspace::tests::production_empty_scenario_lifeline_tool_keeps_staged_source_inside_stable_canvas ... ok
test behavior::scenario::workspace::tests::production_scenario_camera_external_controls_bounds_and_retained_deltas_cancel_old_drags ... ok
test behavior::scenario::workspace::tests::production_scenario_canvas_keeps_bounds_draft_identity_and_camera_through_editing ... ok
test behavior::scenario::workspace::tests::production_scenario_canvas_pan_invalidates_a_pressed_creation_and_caller_restores_once ... ok
test behavior::scenario::workspace::tests::production_scenario_keyboard_enters_activates_draws_focus_and_returns_to_inspector ... ok
test behavior::scenario::workspace::tests::production_scenario_keyboard_reveals_and_activates_initially_offscreen_last_tool ... ok
test behavior::scenario::workspace::tests::production_scenario_retained_toolbar_intents_reject_chooser_dirty_and_picker_lifetimes ... ok
test behavior::scenario::workspace::tests::production_scenario_whole_glyph_drag_moves_one_occurrence_and_rejects_other_operand ... ok
test behavior::scenario::workspace::tests::scenario_form_focus_chained_apply_rechecks_live_review_owner ... ok
test behavior::scenario::workspace::tests::scenario_form_focus_missing_exact_target_leaves_other_inputs_untouched ... ok
test behavior::scenario::workspace::tests::scenario_form_focus_returns_from_back_use_and_escape_after_initial_focus_is_consumed ... ok
test behavior::scenario::workspace::tests::scenario_form_focus_saved_action_is_inert_on_replacement_child_review_and_changed_context ... ok
test behavior::scenario::workspace::tests::scenario_form_focus_targets_the_name_and_rejects_stale_sessions_or_modals ... ok
test behavior::scenario::workspace::tests::scenario_workspace_choosing_tools_preserves_valid_and_invalid_pending_editors ... ok
test behavior::scenario::workspace::tests::scenario_workspace_fit_uses_measured_inner_viewport_and_rejects_nonfinite_inputs ... ok
test behavior::scenario::workspace::tests::scenario_workspace_open_and_back_restore_owner_and_mounted_camera_after_preselection ... ok
test behavior::scenario::workspace::tests::scenario_workspace_same_size_owner_and_document_changes_remeasure_fit_viewport ... ok
test behavior::scenario::workspace::tests::scenario_workspace_scaled_and_panned_scene_keeps_clicked_slots_and_message_endpoints ... ok
test behavior::scenario::workspace::tests::scenario_workspace_scaled_nested_occurrences_and_reorder_keep_exact_sequence_identity ... ok
test behavior::scenario::workspace::tests::scenario_workspace_viewport_events_are_passive_guarded_and_do_not_apply_raw_forms ... ok
test behavior::scenario_lifecycle::tests::alternative_death_does_not_kill_its_displayed_sibling ... ok
test behavior::scenario_lifecycle::tests::conditional_birth_does_not_infer_absence_in_siblings_or_ancestors ... ok
test behavior::scenario_lifecycle::tests::explicit_receiver_is_authoritative_even_with_missing_exchange ... ok
test behavior::scenario_lifecycle::tests::lifecycle_free_fragments_do_not_poison_other_lanes ... ok
test behavior::scenario_lifecycle::tests::loop_lifetime_is_local_and_warning_bearing_loops_remain_drawable ... ok
test behavior::scenario_lifecycle::tests::missing_geometry_is_reported_without_nonfinite_or_guessed_markers ... ok
test behavior::scenario_lifecycle::tests::optional_cleanup_inside_loop_does_not_claim_balanced_or_definite_exit ... ok
test behavior::scenario_lifecycle::tests::projection_retains_exact_authored_source_and_does_not_enter_history ... ok
test behavior::scenario_lifecycle::tests::root_lifetime_has_exact_birth_death_boundaries_without_source_mutation ... ok
test behavior::scenario_lifecycle::tests::scenario_use_is_opaque_and_never_imports_callee_lifecycle_events ... ok
test behavior::scenario_scroll::tests::direct_scenario_caller_restores_mounted_scroll_once_with_clamping_and_app_receipt ... ok
test behavior::scenario_scroll::tests::direct_scenario_restore_cannot_scroll_another_document_owner_or_process_view ... ok
test behavior::scenario_scroll::tests::direct_scroll_mounted_identity_resets_navigation_but_preserves_edits_before_first_report ... ok
test behavior::scenario_scroll::tests::direct_scroll_observer_reports_actual_initial_clamped_and_remounted_offsets_once ... ok
test behavior::scenario_scroll::tests::independent_process_owner_change_resets_actual_scroll_offsets ... ok
test behavior::state_canvas::tests::context_menu_keeps_window_anchor_and_explicit_placement_identity ... ok
test behavior::state_canvas::tests::context_tool_and_pending_form_changes_cancel_mutating_gestures ... ok
test behavior::state_canvas::tests::creation_uses_the_explicit_deepest_owner_and_never_a_fallback_ancestor ... ok
test behavior::state_canvas::tests::drag_uses_ordered_release_at_each_zoom_and_moves_descendants_in_one_undo ... ok
test behavior::state_canvas::tests::escape_and_view_switch_discard_the_armed_transition_source ... ok
test behavior::state_canvas::tests::internal_rows_and_pseudostate_hits_use_shared_persisted_geometry ... ok
test behavior::state_canvas::tests::ordered_stationary_click_does_not_move_on_later_motion_in_the_same_batch ... ok
test behavior::state_canvas::tests::pan_is_button_scoped_read_only_and_hover_does_not_reproject_the_scene ... ok
test behavior::state_canvas::tests::pseudostate_names_avoid_rightward_and_downward_routes_and_share_hit_bounds ... ok
test behavior::state_canvas::tests::relayout_does_not_turn_a_transition_click_into_a_drag_or_pan ... ok
test behavior::state_canvas::tests::selected_endpoint_reconnect_preserves_identity_and_requires_handle_motion ... ok
test behavior::state_canvas::tests::selection_relayout_does_not_move_nodes_or_change_drag_distance ... ok
test behavior::state_canvas::tests::stacked_symbol_names_avoid_each_other_and_are_independent_of_input_order ... ok
test behavior::state_canvas::tests::transition_supports_click_click_and_drag_without_coalesced_accidental_edges ... ok
test behavior::state_canvas::tests::typed_state_actions_are_named_selectable_rows_before_internal_transitions_and_regions ... ok
test behavior::state_canvas::tests::viewport_uses_full_canvas_coordinates_and_republishes_for_a_new_context ... ok
test behavior::state_edit::tests::an_empty_machine_has_an_explicit_visible_owner_for_first_region_creation ... ok
test behavior::state_edit::tests::connect_preserves_self_loops_and_rejects_hidden_or_incompatible_vertices ... ok
test behavior::state_edit::tests::creation_places_missing_ancestry_and_unfolds_existing_owners_atomically ... ok
test behavior::state_edit::tests::every_context_component_and_invalid_ownership_are_checked_before_planning ... ok
test behavior::state_edit::tests::placing_existing_content_and_moving_a_folded_group_preserve_other_views_and_semantics ... ok
test behavior::state_edit::tests::reconnect_preserves_the_complete_record_and_replaces_cardinality_in_place ... ok
test behavior::state_edit::tests::view_initializes_only_its_machine_in_one_source_and_model_history_entry ... ok
test behavior::states::tests::internal_choice_targets_self_and_edit_preserves_identity_and_description ... ok
test behavior::states::tests::invalid_pseudo_trigger_keeps_draft_and_does_not_create_orphan_event ... ok
test behavior::states::tests::selected_composite_owns_new_region_and_history_uses_selected_region ... ok
test behavior::states::tests::timeout_and_work_trigger_commit_together_roundtrip_and_undo ... ok
test behavior::tests::failed_document_commit_retains_typed_transition_and_time_event_for_repair ... ok
test behavior::tests::legacy_process_and_chain_keep_their_ordered_exchange_scene ... ok
test behavior::tests::one_behavior_form_survives_start_edit_and_unrelated_cancel_until_commit ... ok
test behavior::tests::recovery_preserves_distinct_qualified_value_paths_and_rejects_invalid_payload_atomically ... ok
test behavior::tests::recovery_retains_each_incomplete_behavior_form_without_executing_it ... ok
test behavior::tests::selecting_state_transition_keeps_its_behavior_context ... ok
test behavior::topology::contextual::tests::qualified_link_gui_deleted_transient_selection_does_not_seed_or_hide_retained_authored_endpoint ... ok
test behavior::topology::contextual::tests::qualified_link_gui_nested_preview_folds_only_the_exact_reference_instance ... ok
test behavior::topology::contextual::tests::qualified_link_gui_rejected_endpoint_keeps_exact_raw_intent_after_recovery ... ok
test behavior::topology::contextual::tests::qualified_link_gui_repeated_sources_remain_distinct_and_apply_has_exact_source_history ... ok
test behavior::topology::contextual::tests::qualified_link_gui_scope_paging_and_search_reach_tail_beyond_512_without_losing_endpoints ... ok
test behavior::topology::contextual_routing::tests::contextual_route_native_x_to_first_b_keeps_visible_arrow_above_background_and_clear_of_other_references ... ok
test behavior::topology::contextual_routing::tests::contextual_route_nested_visible_hidden_and_parallel_paths_keep_exact_addresses_and_legacy_routes ... ok
test behavior::topology::contextual_routing::tests::contextual_route_shared_anchor_preserves_selected_direct_reconnect_at_fit_and_full_scale ... ok
test behavior::topology::direct_tests::direct_process_append_preserves_existing_order_and_rejects_incompatible_exchange ... ok
test behavior::topology::direct_tests::direct_process_controls_allow_incomplete_work_and_preserve_branch_semantics ... ok
test behavior::topology::direct_tests::direct_process_empty_first_and_second_exchange_keep_the_same_interactive_renderer ... ok
test behavior::topology::direct_tests::direct_process_imported_order_is_projected_without_mutation_and_first_canvas_edit_is_atomic ... ok
test behavior::topology::direct_tests::direct_process_incompatible_imported_order_rejects_conversion_without_losing_draft ... ok
test behavior::topology::direct_tests::direct_process_nested_source_reopen_and_api_command_use_one_transaction ... ok
test behavior::topology::direct_tests::direct_process_oa_sa_repeated_occurrences_share_sources_and_split_one_guarded_link ... ok
test behavior::topology::direct_tests::direct_process_palette_preserves_fit_and_manual_camera ... ok
test behavior::topology::direct_tests::direct_process_production_click_click_drag_insert_and_camera_coordinates ... ok
test behavior::topology::direct_tests::direct_process_production_endpoint_and_peer_reorder_handles_publish_exact_identity ... ok
test behavior::topology::direct_tests::direct_process_production_stale_focus_scroll_bounds_and_pending_cancel ... ok
test behavior::topology::direct_tests::direct_process_readable_default_fit_expansion_and_bounded_choices ... ok
test behavior::topology::direct_tests::direct_process_real_picker_stays_bounded_and_search_keeps_selection ... ok
test behavior::topology::direct_tests::direct_process_reconnect_delete_and_reorder_preserve_other_paths_and_sources ... ok
test behavior::topology::direct_tests::direct_process_reuse_preview_is_local_expandable_and_recursive_sources_are_rejected ... ok
test behavior::topology::direct_tests::direct_process_scaled_control_and_link_gestures_use_visible_positions ... ok
test behavior::topology::direct_tests::direct_process_specific_parallel_link_and_incompatible_adjacency ... ok
test behavior::topology::direct_tests::direct_process_three_ordered_exchanges_preserve_outer_paths_on_insertion ... ok
test behavior::topology::tests::cycle_projection_terminates_and_retains_actual_links ... ok
test behavior::topology::tests::empty_process_can_start_through_normal_authoring_events ... ok
test behavior::topology::tests::fitting_a_branch_keeps_all_nodes_visible_and_selection_uses_scaled_coordinates ... ok
test behavior::topology::tests::guarded_branch_is_one_transaction_with_distinct_source_identities ... ok
test behavior::topology::tests::insertion_picker_only_offers_work_matching_adjacent_exchanges ... ok
test behavior::topology::tests::legacy_branch_insertion_replaces_only_the_selected_implied_link ... ok
test behavior::topology::tests::legacy_edit_preserves_order_and_new_work_in_one_undo ... ok
test behavior::topology::tests::long_engineering_identifiers_stay_inside_label_width ... ok
test behavior::topology::tests::process_camera_selection_ignores_independent_details_scroll ... ok
test behavior::topology::tests::process_fit_disclosures_and_focus_preserve_rejected_raw_draft_and_accepted_source ... ok
test behavior::topology::tests::process_fit_uses_measured_height_and_reports_minimum_without_changing_manual_zoom ... ok
test behavior::topology::tests::reusable_preview_preserves_identity_and_filters_recursive_sources ... ok
test behavior::topology::tests::stale_or_invalid_submit_preserves_inputs_and_creates_nothing ... ok
test behavior::topology::tests::unlinked_contributions_do_not_gain_fictional_edges ... ok
test behavior::topology::view::layout_tests::production_canvas_keyboard_and_delayed_tasks_respect_real_chooser_and_review_owners ... ok
test behavior::topology::view::layout_tests::production_process_caller_return_rejects_its_previous_camera_lifetime ... ok
test behavior::topology::view::layout_tests::production_process_camera_external_controls_bounds_and_retained_deltas_cancel_old_drags ... ok
test behavior::topology::view::layout_tests::production_process_canvas_focus_wheel_fit_and_stale_camera_are_presentation_only ... ok
test behavior::topology::view::layout_tests::production_process_canvas_scaled_path_creation_preserves_source_and_editor_bounds ... ok
test behavior::topology::view::layout_tests::production_process_form_replacement_resets_scroll_without_a_closed_render ... ok
test behavior::topology::view::layout_tests::production_process_keyboard_enters_activates_draws_focus_and_returns_to_inspector ... ok
test behavior::topology::view::layout_tests::production_process_keyboard_reveals_and_activates_initially_offscreen_last_tool ... ok
test behavior::topology::view::layout_tests::production_process_retained_toolbar_intents_reject_chooser_dirty_and_picker_lifetimes ... ok
test connection_edit::tests::cancel_navigation_revision_and_dirty_drafts_cannot_commit_a_stale_connection ... ok
test connection_edit::tests::visible_endpoint_drawing_commits_ports_and_exchange_once_with_exact_history ... ok
test connection_tool::tests::boundary_candidates_match_core_orientation_and_reject_same_owner_or_mixed_kind ... ok
test connection_tool::tests::boundary_feasibility_keeps_interface_warnings_and_final_carried_item_checks ... ok
test connection_tool::tests::boundary_feasibility_rejects_dangling_identity_in_a_malformed_element_map ... ok
test connection_tool::tests::direct_connections_share_exact_source_history_and_model_parity ... ok
test connection_tool::tests::explicit_function_ports_keep_owners_contracts_and_do_not_invent_carried_items ... ok
test connection_tool::tests::final_command_validation_rejects_invalid_models_without_mutation ... ok
test connection_tool::tests::function_cards_use_unique_reuse_and_atomic_generation_when_ambiguous ... ok
test connection_tool::tests::mismatched_port_owners_layers_and_contract_kinds_are_not_candidates ... ok
test connection_tool::tests::operational_communication_uses_participants_without_creating_ports ... ok
test connection_tool::tests::operational_interaction_targets_only_distinct_oa_activities ... ok
test context_menu::tests::a_new_context_token_cannot_reuse_the_previous_mouse_press_or_scroll ... ok
test context_menu::tests::actions_area_does_not_swallow_first_independent_click_after_overlay_consumed_its_release ... ok
test context_menu::tests::anchors_outside_the_window_and_nonfinite_input_stay_bounded ... ok
test context_menu::tests::clicking_disabled_or_dragging_off_an_action_never_executes_it ... ok
test context_menu::tests::coalesced_dismissal_release_does_not_swallow_the_next_independent_click ... ok
test context_menu::tests::empty_and_disabled_only_menus_still_capture_keys_and_escape_closes ... ok
test context_menu::tests::keyboard_skips_headers_separators_and_disabled_actions_and_invokes_once ... ok
test context_menu::tests::outside_dismissal_consumes_press_and_release_across_controlled_close ... ok
test context_menu::tests::reconstructed_overlay_uses_retained_layout_for_rows_and_hit_testing ... ok
test context_menu::tests::retained_overlay_layout_keeps_measured_width_and_scrollbar_after_reconstruction ... ok
test context_menu::tests::tall_menu_clamps_to_viewport_and_keyboard_wheel_and_scrollbar_reach_the_end ... ok
test data_values::tests::phase_choices_keep_reused_record_addresses_distinct ... ok
test data_values::tests::recursive_picker_starts_with_distinct_leaf_names_and_sorts_them ... ok
test data_values::tests::recursive_records_can_be_drilled_into_and_valid_imported_paths_resolve ... ok
test data_values::tests::reference_parameter_stops_descent_and_unspecified_direction_remains_visible ... ok
test data_values::tests::roots_and_one_hop_children_do_not_expand_unrelated_payload_branches ... ok
test data_view::tests::allocation_rows_edit_relationship_identity_and_remove_only_that_allocation ... ok
test data_view::tests::association_creation_is_a_separate_relation_with_editable_end_contracts ... ok
test data_view::tests::browsing_pages_preserves_invalid_numeric_draft_and_inherited_allocation_identity ... ok
test data_view::tests::contextual_data_actions_use_typed_defaults_and_one_undo ... ok
test data_view::tests::large_inherited_dictionary_has_bounded_widgets_and_every_original_field_is_reachable ... ok
test diagram::lifecycle_tests::menu_reset_and_draw_use_fresh_open_context ... ok
test diagram::lifecycle_tests::middle_and_arrow_panning_preserve_source_and_history ... ok
test diagram::lifecycle_tests::motion_after_release_must_not_move_clicked_port ... ok
test diagram::lifecycle_tests::motion_after_source_click_release_must_not_create_connection ... ok
test diagram::pointer_tests::coalesced_fe_ports_commit_once_through_app_and_source_history ... ok
test diagram::pointer_tests::coalesced_port_drag_commits_once_through_app_and_source_history ... ok
test diagram::pointer_tests::drag_from_already_selected_stationary_port_keeps_source_between_batches ... ok
test diagram::pointer_tests::menu_captured_motion_must_not_turn_following_click_into_old_port_drag ... ok
test diagram::pointer_tests::modal_and_stale_epoch_block_queued_connection_and_port_commits ... ok
test diagram::port_hit_tests::coincident_ports_keep_selection_then_use_stable_identity ... ok
test diagram::port_hit_tests::nearest_port_center_wins_on_all_borders_and_zoom_levels ... ok
test diagram::port_hit_tests::nested_owner_palette_selection_drag_and_context_menu_pick_the_same_port ... ok
test diagram::port_labels::tests::leaving_and_reentering_keeps_suppression_until_each_held_button_releases ... ok
test diagram::port_labels::tests::menu_epoch_clears_suppression_when_the_overlay_consumes_release ... ok
test diagram::port_labels::tests::outside_presses_do_not_suppress_and_cancel_or_focus_loss_clear_held_buttons ... ok
test diagram::port_labels::tests::passive_layer_leaves_port_drag_selection_and_commit_with_the_interactive_canvas ... ok
test diagram::tests::association_end_contracts_share_svg_geometry_and_select_without_moving_name_label ... ok
test diagram::tests::bend_gestures_use_screen_hit_radius_and_commit_one_undo_at_three_zooms ... ok
test diagram::tests::coincident_line_hit_retains_the_selected_connection ... ok
test diagram::tests::connection_gestures_pick_real_ports_and_commit_coalesced_drag_at_every_zoom ... ok
test diagram::tests::detached_label_selects_connection_and_occluding_card_owns_its_hit ... ok
test diagram::tests::dragging_a_child_previews_the_same_enclosure_and_ports_as_commit ... ok
test diagram::tests::fast_drags_commit_the_release_cursor_without_a_final_motion_event ... ok
test diagram::tests::implicit_port_click_selects_semantics_and_drag_preview_svg_share_anchors ... ok
test diagram::tests::route_cache_ignores_pan_zoom_and_cancelled_drag_cannot_commit ... ok
test diagram::tests::routed_svg_has_same_polyline_label_and_bounds_as_canvas_scene ... ok
test diagram::tests::svg_distinguishes_inferred_work_allocation_from_direct_allocation ... ok
test diagram::tests::svg_preserves_visible_exchange_labels_and_capability_stereotypes ... ok
test diagram::tests::typed_function_glyphs_and_unoriented_standard_connections_have_distinct_svg ... ok
test diagram::view_options_tests::communication_svg_uses_saved_visibility_labels_and_direction ... ok
test document::save_as_tests::save_as_preserves_explicit_source_entry_changes_in_history ... ok
test document::save_as_tests::save_as_rebases_uniform_history_without_an_undo_step_or_lost_redo ... ok
test document::save_as_tests::save_as_rejects_included_filename_collisions_in_past_and_future_without_mutation ... ok
test document::save_as_tests::save_as_rejects_stale_or_content_changing_receipt_atomically ... ok
test document::tests::accepted_history_keeps_the_latest_hundred_complete_snapshots ... ok
test document::tests::altered_compiled_model_or_index_is_rejected_without_mutation ... ok
test document::tests::asynchronous_save_marks_its_snapshot_without_cleaning_later_edits ... ok
test document::tests::byte_budget_trims_oldest_history_without_discarding_a_large_current ... ok
test document::tests::cached_view_index_tracks_accepted_creation_undo_and_redo ... ok
test document::tests::comment_and_semantic_history_restore_exact_bytes_and_branch_together ... ok
test document::tests::creation_selects_the_single_uuid_persisted_in_source ... ok
test document::tests::invalid_project_is_rejected_before_format_can_normalize_it ... ok
test document::tests::nested_cross_file_reparent_and_source_edit_share_exact_history ... ok
test document::tests::no_op_command_or_source_apply_preserves_history_revision_and_redo ... ok
test document::tests::omitted_id_stabilization_remains_dirty_until_exact_source_is_saved ... ok
test document::tests::recreated_equal_record_does_not_inherit_deleted_source_ownership ... ok
test document::tests::rejected_compile_command_and_writeback_preserve_state_and_redo ... ok
test explorer::tests::collapse_and_search_retain_context_without_changing_collapse_state ... ok
test explorer::tests::deep_containment_and_search_keep_the_full_path_without_recursion ... ok
test explorer::tests::equal_names_have_stable_alias_and_identity_tie_breakers ... ok
test explorer::tests::perspective_isolation_and_allocations_do_not_change_containment ... ok
test explorer::tests::prefix_names_cannot_separate_a_parent_from_its_children ... ok
test forms::data_fields::tests::defaults_distinguish_absent_empty_and_typed_literals_with_escaped_unicode ... ok
test forms::data_fields::tests::field_browser_pages_every_direct_field_without_flattening_other_records ... ok
test forms::data_fields::tests::independent_review_combined_take_type_informs_actual_default_form_in_both_directions ... ok
test forms::data_fields::tests::numeric_bound_drafts_keep_partial_and_full_precision_values_out_of_accepted_state ... ok
test forms::data_fields::tests::selecting_qualified_path_preserves_expression_typed_before_the_next_render ... ok
test forms::data_fields::tests::semantic_reference_choices_follow_selected_mechanism_and_revert_on_keep ... ok
test forms::data_fields::tests::supertype_picker_excludes_descendants_and_keeps_original_inherited_field_identity ... ok
test forms::history_fields::tests::history_absent_baseline_and_empty_history_use_explicit_facts_and_exact_document_history ... ok
test forms::history_fields::tests::history_clear_discards_inactive_invalid_constructors_before_reusing_the_same_row ... ok
test forms::history_fields::tests::history_compound_value_and_constructor_variants_are_editable_without_live_context ... ok
test forms::history_fields::tests::history_deleted_keys_and_observation_uuids_remain_exact_and_duplicate_history_rejects ... ok
test forms::history_fields::tests::history_every_descriptor_fact_choice_matches_actual_closed_types_and_source_contract ... ok
test forms::history_fields::tests::history_exact_strings_do_not_confuse_uuid_shaped_prose_or_literal_keys_with_references ... ok
test forms::history_fields::tests::history_invalid_numbers_survive_constructor_switch_reorder_removal_and_recovery ... ok
test forms::history_fields::tests::history_missing_semantic_field_can_be_added_and_removed_without_touching_prose_or_live_objects ... ok
test forms::history_fields::tests::history_nested_optional_number_events_switch_absent_present_and_recover_invalid_raw ... ok
test forms::history_fields::tests::history_nested_uuid_and_list_events_preserve_present_values_and_tagged_item_branches ... ok
test forms::history_fields::tests::history_optional_uuid_and_text_seed_absence_without_inventing_observations ... ok
test forms::history_fields::tests::history_primitive_type_choices_remain_visible_inside_the_tagged_union ... ok
test forms::history_fields::tests::history_retained_text_constructors_follow_exact_mode_without_changing_the_observation ... ok
test forms::port_fields::tests::all_demo_records_roundtrip_through_graphical_and_escaped_string_drafts ... ok
test forms::port_fields::tests::boundary_choices_honor_draft_direction_same_kind_and_direct_interface_items ... ok
test forms::port_fields::tests::contract_choices_follow_owner_and_never_offer_legacy_or_oa_typed_ports ... ok
test forms::port_fields::tests::failed_shape_keeps_incomplete_text_and_saved_contract_payloads ... ok
test forms::port_fields::tests::functional_endpoints_require_owner_direction_and_items_but_retain_bad_current_choice ... ok
test forms::port_fields::tests::interface_roles_offer_participants_and_component_ports_independent_of_orientation ... ok
test forms::port_fields::tests::port_allocation_choices_use_actual_owner_direction_and_ignore_edited_relation ... ok
test forms::port_fields::tests::standard_switch_retains_incompatible_orientation_until_explicit_repair ... ok
test forms::port_fields::tests::switching_function_direction_preserves_invalid_items_and_each_authored_payload ... ok
test forms::reference_chooser::tests::captured_choice_cannot_cross_document_revision_draft_record_or_array_path ... ok
test forms::reference_chooser::tests::catalog_search_disambiguates_repeated_names_containment_alias_and_exact_placement_identity ... ok
test forms::reference_chooser::tests::dismissal_swallows_matching_release_across_closed_rebuild_but_next_click_reaches_parent ... ok
test forms::reference_chooser::tests::empty_search_escape_tab_and_optional_clear_do_not_silently_choose_nil ... ok
test forms::reference_chooser::tests::historical_paths_never_open_and_only_explicit_choice_or_clear_is_a_write ... ok
test forms::reference_chooser::tests::mounted_clipboard_paste_and_ime_commit_preserve_whole_strings_without_selecting ... ok
test forms::reference_chooser::tests::mounted_filtered_row_click_uses_the_displayed_identity_and_scoped_write_intent ... ok
test forms::reference_chooser::tests::mounted_input_preserves_rapid_same_batch_typing_and_cursor_across_controlled_rebuilds ... ok
test forms::reference_chooser::tests::mounted_keyboard_pages_and_selects_only_the_highlighted_visible_candidate_once ... ok
test forms::reference_chooser::tests::mounted_overlay_clamps_at_three_field_positions_and_keeps_results_and_query_bounded ... ok
test forms::reference_chooser::tests::mounted_query_change_rejects_old_release_and_old_result_events_without_locking_popup ... ok
test forms::reference_chooser::tests::mounted_query_then_escape_in_one_batch_closes_and_relative_keys_keep_input_order ... ok
test forms::reference_chooser::tests::out_of_order_query_serials_cannot_restore_obsolete_results ... ok
test forms::tests::data_and_port_choices_are_typed_but_retain_invalid_current_values ... ok
test forms::tests::editing_allocation_offers_other_performers_without_counting_itself_twice ... ok
test forms::tests::moving_placement_and_changing_identity_is_one_lossless_undoable_edit ... ok
test forms::tests::parent_choices_reject_cycles_without_removing_clear_or_current_choice ... ok
test forms::tests::reply_and_reuse_forms_offer_semantically_compatible_references ... ok
test forms::tests::scenario_choices_respect_owner_and_exchange_layer ... ok
test forms::tests::semantic_port_group_choices_use_combined_owner_and_retain_invalid_transferred_items ... ok
test forms::tests::shared_perspective_change_keeps_payload_and_rejects_incompatible_dependencies ... ok
test forms::tests::state_observation_picker_keeps_work_lifelines_allocated_to_machine_owner ... ok
test forms::tests::state_placement_form_uses_draft_destination_membership_without_changing_other_views ... ok
test forms::tests::state_projection_form_keeps_string_variants_and_requires_a_typed_context ... ok
test forms::view_fields::tests::category_family_change_retains_members_until_explicit_correction ... ok
test forms::view_fields::tests::selective_view_property_apply_recovery_and_undo_preserve_semantics_and_empty_choices ... ok
test guide::tests::filtering_and_related_navigation_keep_selection_visible ... ok
test guide::tests::guide_topic_navigation_resets_only_article_and_same_topic_search_keeps_scroll ... ok
test guide::tests::search_finds_concepts_in_walkthroughs_and_api_reference ... ok
test home::tests::corrupt_future_and_oversized_history_are_preserved_with_usable_session_history ... ok
test home::tests::failed_metadata_replace_keeps_previous_history_and_reports_memory_only_entry ... ok
test home::tests::first_and_replaced_metadata_are_parseable_and_contain_no_model_bytes ... ok
test home::tests::forgotten_and_missing_files_never_change_the_model_or_other_entries ... ok
test home::tests::loading_missing_history_does_not_create_files_or_directories ... ok
test home::tests::malformed_entries_and_unknown_fields_never_become_open_requests ... ok
test home::tests::successful_local_history_is_bounded_ordered_deduplicated_and_durable ... ok
test home::tests::unavailable_location_can_reappear_on_refresh_without_losing_identity ... ok
test home::tests::walkthrough_uses_packaged_bundle_before_development_and_reports_missing_assets ... ok
test hosted::client::tests::hosted_endpoint_validation_redacts_secrets_and_preserves_proxy_paths ... ok
test hosted::client::tests::hosted_lost_publication_response_is_uncertain_without_retry ... ok
test hosted::client::tests::hosted_open_rejects_incoherent_envelopes_missing_etags_and_unstable_ids ... ok
test hosted::client::tests::hosted_oversized_local_bundle_is_rejected_before_network_admission ... ok
test hosted::client::tests::hosted_production_auth_proxy_prefix_and_located_rejection ... ok
test hosted::client::tests::hosted_production_roundtrip_preserves_comments_noops_and_conflicts ... ok
test hosted::client::tests::hosted_publication_race_keeps_receipt_identity_for_review ... ok
test hosted::client::tests::hosted_publication_retains_known_resource_after_failed_followup ... ok
test hosted::client::tests::hosted_put_preserves_an_opaque_etag_and_compiles_the_accepted_bundle ... ok
test hosted::client::tests::hosted_redirect_is_not_followed_or_retried_and_problem_tokens_are_redacted ... ok
test hosted::client::tests::hosted_response_limits_cover_declared_and_chunked_bodies ... ok
test hosted::client::tests::hosted_structured_mutation_failures_distinguish_precommit_rejections ... ok
test hosted::client::tests::hosted_timeout_and_disconnect_leave_mutation_outcomes_explicit ... ok
test model_menu::tests::add_bend_here_uses_context_point_and_one_source_aware_history_change ... ok
test model_menu::tests::late_menu_actions_do_not_dismiss_new_menu_or_cross_view_context ... ok
test model_menu::tests::menu_preserves_pending_properties_and_rejects_delayed_mutations ... ok
test model_menu::tests::pending_source_allows_file_navigation_but_disables_model_actions ... ok
test model_menu::tests::right_click_targets_actor_without_changing_selection_until_action ... ok
test model_menu::tests::show_from_explorer_places_unfolds_and_keeps_semantic_selection ... ok
test model_menu::tests::state_menu_folds_and_hides_only_its_captured_placement_and_rejects_stale_views ... ok
test model_menu::tests::state_menu_keeps_typed_creation_reachable_and_preserves_pending_properties ... ok
test navigation::tests::semantic_browser_finds_behavior_users_without_confusing_containment ... ok
test port_edit::tests::all_border_drags_commit_exact_preview_and_restore_automatic_anchors_in_history ... ok
test port_edit::tests::graphical_port_reparent_clears_old_anchors_in_the_same_history_operation ... ok
test port_preview::tests::functional_creation_identifies_new_and_reused_endpoints_and_owners ... ok
test port_preview::tests::legacy_split_discloses_endpoint_routing_item_origins_and_retained_evidence ... ok
test port_preview::tests::reused_boundary_shows_item_union_and_existing_allocating_facts ... ok
test port_preview::tests::synchronization_preview_exposes_contract_shrinkage_and_current_declaration_origin ... ok
test port_workflow::recovery_tests::port_recovery_retains_authored_contract_and_identity_but_rebuilds_preview ... ok
test port_workflow::tests::creating_typed_port_and_rejecting_stale_preview_preserve_drafts_and_identity ... ok
test port_workflow::tests::graphical_function_exchange_and_component_generation_commit_and_undo_whole_contracts ... ok
test port_workflow::tests::legacy_resolution_preserves_original_record_and_applies_as_one_undo ... ok
test port_workflow::tests::port_creation_keeps_owner_diagram_and_unfolds_in_the_same_undo ... ok
test port_workflow::tests::synchronization_uses_active_contract_instead_of_preserved_direction ... ok
test recovery::source5::tests::source5_fingerprint_preserves_historical_field_order_and_diagram_shape ... ok
test recovery::source5_tests::source4_checkpoint_remains_explicitly_unsupported_without_touching_disk ... ok
test recovery::source5_tests::source5_checkpoint_does_not_refresh_unrelated_clean_buffer_baseline ... ok
test recovery::source5_tests::source5_checkpoint_missing_identity_is_not_a_lossless_upgrade ... ok
test recovery::source5_tests::source5_checkpoint_old_diagram_property_shape_remains_editable ... ok
test recovery::source5_tests::source5_checkpoint_port_draft_replans_and_rejects_stale_base ... ok
test recovery::source5_tests::source5_checkpoint_preserves_dirty_invalid_text_and_baseline_exactly ... ok
test recovery::source5_tests::source5_checkpoint_preserves_distinct_unapplied_open_workspace ... ok
test recovery::source5_tests::source5_checkpoint_property_preserves_invalid_raw_input ... ok
test recovery::source5_tests::source5_checkpoint_upgrade_never_rewrites_checkpoint_or_origin ... ok
test recovery::source5_tests::source5_checkpoint_upgrades_only_header_and_clean_editor_baseline ... ok
test recovery::source5_tests::source5_checkpoint_workflow_rebinds_only_the_proven_old_revision ... ok
test recovery::source7_tests::source7_current_combined_contextual_and_operation_drafts_survive_without_execution ... ok
test recovery::source7_tests::source7_explicit_new_fields_cannot_enter_frozen_old_model_profiles ... ok
test recovery::source7_tests::source7_legacy_inactive_constructor_path_is_structural_but_raw_prose_is_not ... ok
test recovery::source7_tests::source7_legacy_profile_workflow_and_semantic_drafts_replan_with_exact_base ... ok
test recovery::source7_tests::source7_legacy_profiles_preserve_dirty_source_property_and_behavior_raw_fields ... ok
test recovery::source7_tests::source7_legacy_structural_intent_rejects_even_null_but_never_scans_raw_strings ... ok
test recovery::source7_tests::source7_legacy_view_selector_refuses_new_relation_kind_without_rewriting_checkpoint ... ok
test recovery::source7_tests::source7_recovers_nonempty_frozen_5_and_6_profiles_with_exact_historical_binding ... ok
test recovery::tests::altered_accepted_identity_and_unrecognized_raw_property_fields_reject ... ok
test recovery::tests::exact_source_and_unfinished_invalid_buffers_restore_without_model_overwrite ... ok
test recovery::tests::excess_retained_buffers_cannot_replace_the_last_recoverable_checkpoint ... ok
test recovery::tests::invalid_open_retains_distinct_accepted_model_and_repair_workspace ... ok
test recovery::tests::missing_includes_remain_reachable_repair_targets_without_fabricated_buffers ... ok
test recovery::tests::multiple_sessions_replace_only_their_own_checkpoint_and_corruption_is_retained ... ok
test recovery::tests::raw_invalid_property_number_survives_and_stays_rejected_until_repaired ... ok
test semantic_review::artifacts_recovery_falsifier_tests::forged_artifact_draft_cannot_target_an_unreserved_or_non_artifact_identity ... ok
test semantic_review::artifacts_recovery_falsifier_tests::forged_artifact_raw_variants_duplicate_addresses_and_unknown_fields_are_refused ... ok
test semantic_review::artifacts_recovery_falsifier_tests::forged_artifact_reservations_cannot_duplicate_or_claim_an_existing_identity_or_another_owner ... ok
test semantic_review::artifacts_recovery_falsifier_tests::legitimate_invalid_artifact_checkpoint_round_trips_and_prepares_only_after_explicit_repair ... ok
test semantic_review::artifacts_tests::artifact_candidate_is_a_complete_record_not_a_tombstone_or_unattached_creation_shortcut ... ok
test semantic_review::artifacts_tests::artifact_creation_and_identified_source_deletion_follow_core_metadata_and_history_boundary_both_directions ... ok
test semantic_review::artifacts_tests::artifact_gui_reserves_recovers_and_creates_only_after_all_seven_metadata_reviews_both_directions ... ok
test semantic_review::artifacts_tests::artifact_metadata_apply_and_exact_history_reset_are_one_source_preserving_undo_redo ... ok
test semantic_review::artifacts_tests::artifact_move_metadata_none_empty_and_invalid_raw_recovery_are_independent_both_directions ... ok
test semantic_review::artifacts_tests::artifact_moved_source_absence_deletes_but_destination_only_take_retains_both_directions ... ok
test semantic_review::attachments::tests::absent_attachment_payloads_migrate_v1_and_v2_and_source5_upgrade_requires_exact_legacy_base ... ok
test semantic_review::attachments::tests::addressed_attachment_candidates_equal_direct_core_oracle_both_directions ... ok
test semantic_review::attachments::tests::attachment_apply_is_one_exact_source_and_history_undo_redo ... ok
test semantic_review::attachments::tests::attachment_candidate_and_direct_record_keep_conflict_on_actual_slot_without_losing_drafts ... ok
test semantic_review::attachments::tests::attachment_pair_drafts_survive_family_switch_and_restore_without_mutating_model ... ok
test semantic_review::attachments::tests::functional_incident_source_requires_explicit_matching_port_review_in_one_atomic_candidate ... ok
test semantic_review::attachments::tests::parent_and_each_endpoint_candidate_preserve_other_fields_and_match_core_both_directions ... ok
test semantic_review::attachments::tests::raw_rejected_attachment_reference_survives_recovery_and_wrong_scope_is_rejected ... ok
test semantic_review::attachments::tests::source_only_absence_and_explicit_absent_endpoint_are_different ... ok
test semantic_review::attachments::tests::two_incident_endpoint_candidates_form_one_complete_record_and_recover_invalid_excluded_draft ... ok
test semantic_review::batch_tests::batch_apply_is_one_exact_source_and_history_undo_redo_roundtrip ... ok
test semantic_review::batch_tests::batch_candidate_conflict_keeps_both_raw_values_and_keep_is_a_real_constraint ... ok
test semantic_review::batch_tests::batch_complete_constructor_cannot_erase_another_rows_new_literal ... ok
test semantic_review::batch_tests::batch_constructor_retirement_conflicts_with_another_rows_kept_bound_in_both_orders ... ok
test semantic_review::batch_tests::batch_delayed_text_targets_its_original_row_and_facet_without_switching_focus ... ok
test semantic_review::batch_tests::batch_excluded_invalid_row_survives_focus_reopen_and_v2_recovery ... ok
test semantic_review::batch_tests::batch_first_equal_constructor_acknowledgement_allows_an_independent_bound ... ok
test semantic_review::batch_tests::batch_joint_bounds_use_one_candidate_and_match_an_independent_atomic_core_oracle ... ok
test semantic_review::batch_tests::batch_reverse_distinct_targets_work_and_reverse_convergence_is_explicit ... ok
test semantic_review::batch_tests::batch_v2_rejects_duplicate_included_sources_bad_addresses_and_stale_model ... ok
test semantic_review::batch_tests::category_family_and_members_are_independent_core_checked_facets_both_directions ... ok
test semantic_review::batch_tests::recovered_trace_picker_names_existing_trace_instead_of_deleted_reference ... ok
test semantic_review::batch_tests::rejected_direction_switch_at_row_limit_preserves_every_recoverable_draft ... ok
test semantic_review::compose::diagnostic_tests::distinct_review_conflict_keeps_trace_aliases_even_when_names_are_equal ... ok
test semantic_review::compose::diagnostic_tests::reserved_creation_conflict_uses_its_rows_seed_instead_of_a_deleted_reference ... ok
test semantic_review::compose::diagnostic_tests::same_review_presence_and_owner_conflict_names_both_facets_and_owners_and_retains_drafts ... ok
test semantic_review::compose::diagnostic_tests::typed_owner_display_does_not_reinterpret_uuid_shaped_artifact_text ... ok
test semantic_review::constructor_tests::closed_data_and_event_take_composition_matches_core_across_every_kind_pair_and_direction ... ok
test semantic_review::constructor_tests::data_group_candidates_cover_every_definition_and_companion_default_both_directions ... ok
test semantic_review::constructor_tests::data_group_retains_invalid_sibling_drafts_companion_default_and_variant_editor_buffers ... ok
test semantic_review::constructor_tests::exchange_constructor_takes_qualified_annotation_addresses_without_remapping_expression_text ... ok
test semantic_review::constructor_tests::existing_fragment_and_branch_group_candidates_edit_complete_owned_order_without_deletion ... ok
test semantic_review::constructor_tests::group_take_reference_payload_and_scalar_candidate_match_core_and_one_document_batch ... ok
test semantic_review::constructor_tests::historical_event_keep_retires_only_obsolete_branch_and_unchanged_kind_acknowledges_no_payload ... ok
test semantic_review::constructor_tests::historical_same_kind_take_preserves_target_only_literals_when_composed_with_candidate ... ok
test semantic_review::constructor_tests::occurrence_and_event_candidate_variants_cover_closed_families_without_touching_headers ... ok
test semantic_review::constructor_tests::outer_and_nested_constructor_candidates_require_explicit_resolution_and_retain_both_drafts ... ok
test semantic_review::constructor_tests::port_group_companions_typed_owner_context_and_direction_restrictions_are_explicit ... ok
test semantic_review::constructor_tests::reverse_existing_typed_port_reviews_keep_or_candidate_legacy_without_permitting_typed_oa_write ... ok
test semantic_review::constructor_tests::scenario_use_group_takes_typed_binding_references_and_preserves_source_text_both_directions ... ok
test semantic_review::keyed_rows_tests::annotation_address_browser_descends_qualified_fields_without_changing_rows ... ok
test semantic_review::keyed_rows_tests::annotation_observations_keep_uuid_shaped_expression_text_in_both_sides_and_history ... ok
test semantic_review::keyed_rows_tests::batch_keyed_rows_compare_exact_target_slots_and_retain_independent_values ... ok
test semantic_review::keyed_rows_tests::binding_choices_separate_reused_keys_from_local_lifelines ... ok
test semantic_review::keyed_rows_tests::binding_local_picker_follows_a_candidate_parent_before_the_review_is_valid ... ok
test semantic_review::keyed_rows_tests::binding_take_requires_local_correspondence_and_candidate_rejects_wrong_scenario ... ok
test semantic_review::keyed_rows_tests::independent_rows_keep_take_candidate_match_core_and_preserve_siblings_both_directions ... ok
test semantic_review::keyed_rows_tests::keyed_recovery_retains_raw_rows_and_staged_pairs_both_directions_without_applying ... ok
test semantic_review::keyed_rows_tests::keyed_row_drafts_retain_expression_local_and_presence_across_decisions_and_staging ... ok
test semantic_review::keyed_rows_tests::keyed_rows_distinguish_unpaired_source_from_explicit_absence_and_require_exact_reset ... ok
test semantic_review::keyed_rows_tests::multiple_new_rows_compose_in_empty_collections_using_destination_key_order ... ok
test semantic_review::keyed_rows_tests::row_review_apply_preserves_exact_source_and_semantic_history_through_undo_redo ... ok
test semantic_review::keyed_tests::deleted_member_source_observation_removes_only_mapped_membership_both_directions ... ok
test semantic_review::keyed_tests::exact_literal_new_rows_and_removals_commit_both_directions_without_default_loss ... ok
test semantic_review::keyed_tests::exact_reset_only_is_one_history_source_transaction_and_undo_redo_both_directions ... ok
test semantic_review::keyed_tests::function_port_members_use_existing_typed_choices_and_preserve_oa_legacy ... ok
test semantic_review::keyed_tests::historical_one_sided_pair_migration_requires_exact_reset_and_explicit_overlap_resolution ... ok
test semantic_review::keyed_tests::invalid_literal_removal_and_unselected_scalar_buffers_are_retained_and_repairable ... ok
test semantic_review::keyed_tests::member_identity_labels_and_new_tabs_preserve_exact_names_and_deleted_keys ... ok
test semantic_review::keyed_tests::reset_precedes_equal_constructor_candidate_and_preserves_independent_literal_draft ... ok
test semantic_review::keyed_tests::staging_removal_and_obsolete_reset_recovery_keep_other_decisions_and_raw_presence ... ok
test semantic_review::keyed_tests::stale_or_source_failed_reset_and_membership_apply_retain_exact_choices ... ok
test semantic_review::keyed_tests::unordered_members_keep_take_candidate_preserve_other_rows_both_directions ... ok
test semantic_review::keyed_tests::unsupported_independent_families_and_missing_identities_do_not_gain_edit_authority ... ok
test semantic_review::message_kind_tests::message_group_candidate_closed_kinds_are_core_validated_in_both_directions ... ok
test semantic_review::message_kind_tests::message_group_candidate_timer_switches_preserve_both_drafts_and_overlap_is_explicit ... ok
test semantic_review::message_kind_tests::message_group_initial_equal_take_acknowledges_only_kind_and_checks_authored_timer_agreement ... ok
test semantic_review::message_kind_tests::message_group_keep_historical_switch_retires_timer_and_completed_no_switch_rejects ... ok
test semantic_review::message_kind_tests::message_group_rejects_protected_edits_stale_plan_and_source_failure_without_losing_timer ... ok
test semantic_review::message_kind_tests::message_group_reply_and_reference_take_require_explicit_call_mapping_both_directions ... ok
test semantic_review::message_kind_tests::message_group_take_full_timer_and_scalar_candidate_are_one_source_history_change_both_directions ... ok
test semantic_review::message_kind_tests::message_group_timer_does_not_erase_exchange_without_its_own_selected_decision ... ok
test semantic_review::ordered_tests::ambiguous_order_preserves_local_member_and_requires_explicit_candidate_positions ... ok
test semantic_review::ordered_tests::historical_pair_restore_uses_exact_member_keys_and_never_zips_order_observations ... ok
test semantic_review::ordered_tests::order_take_preserves_destination_only_positions_and_order_alone_cannot_add_members ... ok
test semantic_review::ordered_tests::ordered_membership_choices_stage_exact_pairs_in_all_owner_shapes ... ok
test semantic_review::ordered_tests::ordered_presence_keep_take_candidate_remove_only_selected_entry_and_order_take_reorders_only ... ok
test semantic_review::ordered_tests::ordered_review_apply_is_exact_source_history_transaction_with_reopen_and_undo_redo ... ok
test semantic_review::ordered_tests::ordered_take_uses_unique_anchors_and_matches_core_with_unrelated_candidate_both_directions ... ok
test semantic_review::ordered_tests::raw_order_survives_membership_changes_decisions_and_recovery_until_explicit_reconcile ... ok
test semantic_review::ordered_tests::several_incoming_members_follow_source_order_despite_independent_identity_sorting ... ok
test semantic_review::recovery::tests::raw_scalar_and_closed_constructor_drafts_recover_against_exact_model_only ... ok
test semantic_review::recovery::tests::version_one_without_orders_restores_raw_intent_and_replans_without_execution ... ok
test semantic_review::source7_tests::contextual_incident_leaf_keep_allows_path_change_but_conflicts_with_changed_leaf_both_directions ... ok
test semantic_review::source7_tests::reverse_operation_omission_gui_requires_raw_rationale_and_matches_one_core_batch_after_recovery ... ok
test semantic_review::source7_tests::semantic_operation_virtual_none_candidate_uses_exact_allocation_and_preserves_accepted_bytes ... ok
test semantic_review::tests::candidate_mixed_with_reference_take_uses_typed_correspondence_not_text_guessing ... ok
test semantic_review::tests::deselected_invalid_candidate_is_retained_but_does_not_poison_another_selected_scalar ... ok
test semantic_review::tests::explicit_trace_selection_updates_only_that_history ... ok
test semantic_review::tests::first_equal_constructor_acknowledges_only_kind_and_allows_independent_leaves ... ok
test semantic_review::tests::independent_review_clear_bound_ignores_retained_invalid_other_facet ... ok
test semantic_review::tests::independent_review_keyed_fact_titles_keep_live_deleted_and_qualified_identity ... ok
test semantic_review::tests::inherited_annotation_impact_requires_exact_acceptance_and_edits_clear_approval ... ok
test semantic_review::tests::invalid_numeric_draft_and_protected_edits_retain_workflow_and_source ... ok
test semantic_review::tests::read_only_structured_facts_and_partial_rollup_are_honest ... ok
test semantic_review::tests::relation_candidates_preserve_endpoints_and_combine_selected_roles_and_bounds ... ok
test semantic_review::tests::scalar_keep_take_candidate_commit_exact_source_and_history_in_both_directions ... ok
test semantic_review::tests::scoped_literal_list_edits_preserve_unfinished_values_and_other_facet_buffers ... ok
test semantic_review::tests::stale_plan_and_source_writeback_failure_do_not_lose_decisions_or_history ... ok
test semantic_review::tests::state_kind_scalar_uses_typed_candidate_and_core_validation ... ok
test semantic_review::view::reserved_artifact_tests::reserved_artifact_facet_and_reference_labels_survive_recovery_without_calling_it_deleted ... ok
test source_navigation::tests::search_uses_utf8_columns_wraps_and_refreshes_after_edits ... ok
test source_tree::tests::active_scroll_offset_uses_the_visible_order_and_retains_ancestor_context ... ok
test source_tree::tests::collapsed_folders_retain_active_dirty_and_diagnostic_markers ... ok
test source_tree::tests::deep_paths_build_expand_and_drop_on_a_small_stack ... ok
test source_tree::tests::detached_buffers_keep_paths_and_only_clean_buffers_are_closable ... ok
test source_tree::tests::exact_unicode_spaces_and_file_folder_name_collisions_are_not_normalized ... ok
test source_tree::tests::folders_are_compact_and_sorted_before_files_without_losing_duplicate_basenames ... ok
test source_workspace::tests::draft_history_budget_covers_inactive_files_without_discarding_drafts ... ok
test source_workspace::tests::switching_retains_invalid_drafts_and_independent_history ... ok
test tests::actor_port_creation_updates_containment_and_locate_reveals_its_owner ... ok
test tests::adding_bend_between_close_ports_stays_outside_owner_clearance ... ok
test tests::behavior_and_data_authoring_do_not_place_nodes_in_unrelated_architecture_views ... ok
test tests::bidirectional_transition_ui_commits_once_and_preserves_other_diagrams ... ok
test tests::counterpart_creation_focuses_visible_target_and_reuse_keeps_views ... ok
test tests::creation_from_capabilities_routes_work_to_architecture_and_folded_allocation_inserts_below_existing_work ... ok
test tests::dragging_keeps_semantic_selection_and_same_node_remains_editable ... ok
test tests::escaped_string_mode_preserves_exact_control_characters ... ok
test tests::every_demo_record_has_a_lossless_graphical_draft ... ok
test tests::explicit_relationship_builder_never_mutates_on_preview ... ok
test tests::explorer_and_creation_selection_follow_their_own_placement ... ok
test tests::fit_adapts_to_canvas_returning_after_help_but_preserves_manual_navigation ... ok
test tests::fit_uses_measured_canvas_bounds_and_preserves_them_on_open ... ok
test tests::focused_explorer_follows_reparenting_without_changing_identity ... ok
test tests::graphical_rename_keeps_aliases_and_references_and_undo ... ok
test tests::identity_property_change_is_atomic_and_rewrites_all_live_references ... ok
test tests::invalid_numeric_draft_survives_navigation_save_and_help ... ok
test tests::invalid_save_and_continue_returns_to_repairable_draft ... ok
test tests::malformed_source_preserves_model_and_is_recoverable ... ok
test tests::manual_trace_requires_target_and_selection_normalizes_creation_kind ... ok
test tests::mission_capability_authoring_is_contextual_atomic_and_text_equivalent ... ok
test tests::modal_owns_background_commands_and_shortcuts ... ok
test tests::nested_allocation_insertion_reflows_sibling_subtrees_and_preserves_fold_state ... ok
test tests::opening_and_creating_models_preserve_the_actual_window_size_for_fit ... ok
test tests::opening_legacy_source_surfaces_report_without_modifying_original_bytes ... ok
test tests::pending_source_prevents_property_draft_collision_and_preserves_both_if_present ... ok
test tests::placement_identity_edit_then_drag_and_diagram_identity_then_arrange ... ok
test tests::placement_relocation_follows_cross_layer_traceability_view ... ok
test tests::property_enum_and_optional_reference_roundtrip ... ok
test tests::reopening_canvas_for_a_modal_preserves_viewport_after_route_edits ... ok
test tests::revealing_an_implicit_function_port_unfolds_its_visible_architecture_owner ... ok
test tests::routing_actions_are_atomic_guarded_and_fit_the_authored_detour ... ok
test tests::selecting_current_constructors_preserves_payloads ... ok
test tests::source_find_navigates_an_invalid_draft_without_applying_or_losing_it ... ok
test tests::source_navigation_waits_for_editor_layout_and_cancels_on_edit ... ok
test tests::system_only_models_open_the_populated_perspective ... ok
test tests::transition_resolutions_belong_to_the_effective_target_and_kind ... ok
test view_copy::tests::duplicate_retains_ports_routes_options_and_artifact_claims_with_exact_undo_redo ... ok
test view_copy::tests::duplicate_state_view_preserves_machine_nesting_and_collapse_with_unique_aliases ... ok
test view_copy::tests::missing_view_does_not_propose_an_empty_replacement ... ok
test views::traceability::tests::traceability_browsing_retains_invalid_property_draft_and_existing_select_guard ... ok
test views::traceability::tests::traceability_filter_reaches_artifacts_after_68_traces_and_searches_all_metadata ... ok
test workflow::communication::tests::communication_requires_two_explicit_ports_and_recovers_the_partial_choice ... ok
test workflow::port_authoring_tests::boundary_picker_matches_exact_commands_across_contracts_and_ownership ... ok
test workflow::port_authoring_tests::boundary_picker_recomputes_for_unoriented_reverse_endpoints_without_relaxing_contracts ... ok
test workflow::port_authoring_tests::duplicate_port_names_show_owner_contract_and_unique_alias ... ok
test workflow::port_authoring_tests::fresh_functions_are_actual_picker_targets_and_plan_enables_apply ... ok
test workflow::port_authoring_tests::generic_component_port_connection_keeps_its_endpoint_and_direction ... ok
test workflow::port_authoring_tests::ordinary_standard_connections_default_unoriented_and_new_roles_exclude_legacy ... ok
test workflow::recovery::tests::incomplete_relationship_and_trace_recover_raw_fields_and_reject_changed_model ... ok
test workflow::recovery::tests::transition_recovery_replans_typed_request_without_persisted_commands ... ok
test workflow::transition_tests::basic_transition_boundary_choice_is_required_without_an_inferred_system ... ok
test workflow::transition_tests::basic_transition_creates_overview_when_existing_views_cannot_show_result ... ok
test workflow::transition_tests::basic_transition_explicit_destination_appends_below_existing_bounds_and_is_one_source_edit ... ok
test workflow::transition_tests::basic_transition_invalid_explicit_destination_retains_choice_and_refuses ... ok
test workflow::transition_tests::basic_transition_reconcile_keeps_existing_selected_view_placement_and_route_exact ... ok
test workflow::transition_tests::basic_transition_skips_lowest_uuid_state_view_and_preserves_oa ... ok
test result: ok. 792 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 296.11s
@@ -0,0 +1,19 @@
{
"source": "d9b3f0dc91e8882e6efc262c2e8d971998d2694e",
"start": "2026-09-12T11:18:56.702179+00:00",
"end": "2026-09-12T11:26:43.562705+00:00",
"exit": 0,
"command": [
"cargo",
"test",
"--locked",
"-p",
"oa-desktop",
"--bin",
"arcadia-studio",
"--",
"--test-threads=1"
],
"inputs": "full-d9b3f0d.inputs.json",
"changed_inputs": []
}
@@ -0,0 +1,32 @@
{
"saved_at_utc": "2026-09-11T17:08:45.0304039Z",
"note": "Closed Windows gate binary and runtime DLL preserved without launching either.",
"files": [
{
"source": "target/debug/deps/oa_core-73987e0d7089bf1e.exe",
"destination": "binary/oa_core-73987e0d7089bf1e.exe",
"bytes": 34962432,
"sha256": "1B7E020F35312FB8ACB163ED1A9C29890CDD4379C03E238D47BA46575E35CC6F",
"source_after_sha256": "1B7E020F35312FB8ACB163ED1A9C29890CDD4379C03E238D47BA46575E35CC6F",
"destination_sha256": "1B7E020F35312FB8ACB163ED1A9C29890CDD4379C03E238D47BA46575E35CC6F"
},
{
"source": ".local/toolchain/llvm-mingw-20260908-ucrt-x86_64/bin/libunwind.dll",
"destination": "binary/libunwind.dll",
"bytes": 190464,
"sha256": "AA282FA47BB2C4E210CEA5E470CDBDCA58568F0BE0BC73E404D86AB9CD95EF6A",
"source_after_sha256": "AA282FA47BB2C4E210CEA5E470CDBDCA58568F0BE0BC73E404D86AB9CD95EF6A",
"destination_sha256": "AA282FA47BB2C4E210CEA5E470CDBDCA58568F0BE0BC73E404D86AB9CD95EF6A"
},
{
"source": ".local/toolchain/rustup/toolchains/stable-x86_64-pc-windows-gnullvm/bin/libunwind.dll",
"destination": "runtime-alternates/rust-stable/libunwind.dll",
"bytes": 90624,
"sha256": "13BF4E99B0193634EBDEB0BBDEFF9753B39F1D04799183F664111014AABF4C2C",
"source_after_sha256": "13BF4E99B0193634EBDEB0BBDEFF9753B39F1D04799183F664111014AABF4C2C",
"destination_sha256": "13BF4E99B0193634EBDEB0BBDEFF9753B39F1D04799183F664111014AABF4C2C"
}
],
"runtime_verified_at_utc": "2026-09-11T17:13:54.5943060Z",
"runtime_provenance": "Root reports every gate dot-sourced .local/toolchain/env.ps1, which prepended CARGO_HOME/bin then LLVM-MinGW bin. Environment-path libunwind is staged beside binary; identical current stable host/target runtime is separately preserved as an alternative. Cargo may add search paths. No loaded-module capture was taken, so no definitive loaded DLL binding is claimed."
}
@@ -0,0 +1,120 @@
{
"captured_at_utc": "2026-09-11T17:21:35.7042531Z",
"logs": [
{
"name": "analysis-codec-subprocess-first-20260911.log",
"source": ".local/logs/analysis-codec-subprocess-first-20260911.log",
"source_last_write_utc": "2026-09-11T16:48:01.0537466Z",
"command": "cargo test --locked -p oa-core --lib persistence::json_codec::tests::codec_lifecycle_subprocess -- --exact --test-threads=1 --nocapture",
"terminal_exit_code": 0,
"exit_code_provenance": "Root-reported terminal result; raw log has no shell status trailer.",
"observed": "Driver 1 passed, 98 filtered; accepted/refused/malformed/ownership/atomic children each exit code 0 on configured 131072-byte threads.",
"elapsed": "Compile 19m16s; driver 6.08s",
"carriage_return_bytes": 71,
"local_copy": {
"bytes": 3104,
"sha256": "BCD053C7226E17171BF127BE8B3A27A2F75ED13516674AE2DF78D09F5353A21F",
"source_after_sha256": "BCD053C7226E17171BF127BE8B3A27A2F75ED13516674AE2DF78D09F5353A21F",
"destination_sha256": "BCD053C7226E17171BF127BE8B3A27A2F75ED13516674AE2DF78D09F5353A21F"
},
"durable_copy": {
"bytes": 3104,
"sha256": "BCD053C7226E17171BF127BE8B3A27A2F75ED13516674AE2DF78D09F5353A21F",
"source_after_sha256": "BCD053C7226E17171BF127BE8B3A27A2F75ED13516674AE2DF78D09F5353A21F",
"destination_sha256": "BCD053C7226E17171BF127BE8B3A27A2F75ED13516674AE2DF78D09F5353A21F"
}
},
{
"name": "analysis-codec-six-20260911.log",
"source": ".local/logs/analysis-codec-six-20260911.log",
"source_last_write_utc": "2026-09-11T16:51:35.2692799Z",
"command": "cargo test --locked -p oa-core --lib persistence::json_codec::tests -- --test-threads=1 --nocapture",
"terminal_exit_code": 0,
"exit_code_provenance": "Root-reported terminal result; raw log has no shell status trailer.",
"observed": "6 passed, 93 filtered; five subprocess child phases each exit code 0.",
"elapsed": "Compile/cache finish 2.52s; tests 36.65s",
"carriage_return_bytes": 67,
"local_copy": {
"bytes": 3273,
"sha256": "20DAC6C5C8FEE3FC60EA8C2733EEB7EA438055B08B59D9F8F8C81DE9D72F5EF8",
"source_after_sha256": "20DAC6C5C8FEE3FC60EA8C2733EEB7EA438055B08B59D9F8F8C81DE9D72F5EF8",
"destination_sha256": "20DAC6C5C8FEE3FC60EA8C2733EEB7EA438055B08B59D9F8F8C81DE9D72F5EF8"
},
"durable_copy": {
"bytes": 3273,
"sha256": "20DAC6C5C8FEE3FC60EA8C2733EEB7EA438055B08B59D9F8F8C81DE9D72F5EF8",
"source_after_sha256": "20DAC6C5C8FEE3FC60EA8C2733EEB7EA438055B08B59D9F8F8C81DE9D72F5EF8",
"destination_sha256": "20DAC6C5C8FEE3FC60EA8C2733EEB7EA438055B08B59D9F8F8C81DE9D72F5EF8"
}
},
{
"name": "analysis-codec-analysis28-20260911.log",
"source": ".local/logs/analysis-codec-analysis28-20260911.log",
"source_last_write_utc": "2026-09-11T16:51:40.6379426Z",
"command": "cargo test --locked -p oa-core --lib analysis:: -- --test-threads=1",
"terminal_exit_code": 0,
"exit_code_provenance": "Root-reported terminal result; raw log has no shell status trailer.",
"observed": "28 passed, 71 filtered; 25 schema8 tests plus 3 existing topology_context::analysis tests.",
"elapsed": "Compile/cache finish 0.72s; tests 4.34s",
"carriage_return_bytes": 35,
"local_copy": {
"bytes": 3062,
"sha256": "26FF62565CDA9F2AD64DB31C0634593A052E9BFE03577B641E44AAD2EA8129DF",
"source_after_sha256": "26FF62565CDA9F2AD64DB31C0634593A052E9BFE03577B641E44AAD2EA8129DF",
"destination_sha256": "26FF62565CDA9F2AD64DB31C0634593A052E9BFE03577B641E44AAD2EA8129DF"
},
"durable_copy": {
"bytes": 3062,
"sha256": "26FF62565CDA9F2AD64DB31C0634593A052E9BFE03577B641E44AAD2EA8129DF",
"source_after_sha256": "26FF62565CDA9F2AD64DB31C0634593A052E9BFE03577B641E44AAD2EA8129DF",
"destination_sha256": "26FF62565CDA9F2AD64DB31C0634593A052E9BFE03577B641E44AAD2EA8129DF"
}
},
{
"name": "analysis-codec-numeric26-20260911.log",
"source": ".local/logs/analysis-codec-numeric26-20260911.log",
"source_last_write_utc": "2026-09-11T16:51:41.9775469Z",
"command": "cargo test --locked -p oa-core --lib analysis_numeric::tests -- --test-threads=1",
"terminal_exit_code": 0,
"exit_code_provenance": "Root-reported terminal result; raw log has no shell status trailer.",
"observed": "26 passed, 73 filtered.",
"elapsed": "Compile/cache finish 0.91s; tests 0.14s",
"carriage_return_bytes": 33,
"local_copy": {
"bytes": 2664,
"sha256": "7B5A1B7B7F52CE135F0E406D8E09A092DD0E93AEDD07DF42B2D8776E1D4DA33E",
"source_after_sha256": "7B5A1B7B7F52CE135F0E406D8E09A092DD0E93AEDD07DF42B2D8776E1D4DA33E",
"destination_sha256": "7B5A1B7B7F52CE135F0E406D8E09A092DD0E93AEDD07DF42B2D8776E1D4DA33E"
},
"durable_copy": {
"bytes": 2664,
"sha256": "7B5A1B7B7F52CE135F0E406D8E09A092DD0E93AEDD07DF42B2D8776E1D4DA33E",
"source_after_sha256": "7B5A1B7B7F52CE135F0E406D8E09A092DD0E93AEDD07DF42B2D8776E1D4DA33E",
"destination_sha256": "7B5A1B7B7F52CE135F0E406D8E09A092DD0E93AEDD07DF42B2D8776E1D4DA33E"
}
},
{
"name": "analysis-codec-clippy-20260911.log",
"source": ".local/logs/analysis-codec-clippy-20260911.log",
"source_last_write_utc": "2026-09-11T16:54:56.0732308Z",
"command": "cargo clippy --locked -p oa-core --all-targets -- -D warnings",
"terminal_exit_code": 0,
"exit_code_provenance": "Root-reported terminal result; raw log has no shell status trailer.",
"observed": "Finished dev profile [unoptimized]; no warnings or errors in the closed log.",
"elapsed": "3m13s",
"carriage_return_bytes": 9,
"local_copy": {
"bytes": 410,
"sha256": "3FFF0B9934BECDF65707CB8EDBF5F85FE2C2E1C8053C71EEAE5B3D32494E6218",
"source_after_sha256": "3FFF0B9934BECDF65707CB8EDBF5F85FE2C2E1C8053C71EEAE5B3D32494E6218",
"destination_sha256": "3FFF0B9934BECDF65707CB8EDBF5F85FE2C2E1C8053C71EEAE5B3D32494E6218"
},
"durable_copy": {
"bytes": 410,
"sha256": "3FFF0B9934BECDF65707CB8EDBF5F85FE2C2E1C8053C71EEAE5B3D32494E6218",
"source_after_sha256": "3FFF0B9934BECDF65707CB8EDBF5F85FE2C2E1C8053C71EEAE5B3D32494E6218",
"destination_sha256": "3FFF0B9934BECDF65707CB8EDBF5F85FE2C2E1C8053C71EEAE5B3D32494E6218"
}
}
]
}
@@ -0,0 +1,40 @@
{
"captured_at_utc": "2026-09-11T17:21:38.9339958Z",
"records": [
{
"path": "AGENTS.md",
"root": "root checkout",
"bytes": 10628,
"sha256": "FCF69CF340B4F5A7CF68E79027DF30F4FB52740C9F62FFC80EE0CC05AC881C34"
},
{
"path": "PROJECT_GOAL.md",
"root": "root checkout",
"bytes": 8696,
"sha256": "B43B3DB31B09B6AF2DEE444EB8A06B70A4F4399B53F4744254D62D446D89734B"
},
{
"path": "harness/release-analysis-codec-source-review-20260911.md",
"root": "root checkout",
"bytes": 11712,
"sha256": "78AEC0468B2493DF3C6A64A097F83683572B935B1ABCFCDBDBDEE54F7832E0BB"
},
{
"path": ".local/toolchain/env.ps1",
"root": "root checkout",
"bytes": 331,
"sha256": "ED5B7E8F1105C5355511E08CDEB4B96156FB245BFF430E6BB9237AE8781638CC"
},
{
"path": "harness/release-analysis-codec-implementation-20260911.md",
"root": "isolated 847b1caa",
"bytes": 14366,
"sha256": "38F80E88A3945720336ED65D008CC5F260ECE2830957ADE54DF7561324726169"
}
],
"isolated_clean_after": true,
"profile": "Windows x86_64-pc-windows-gnullvm; test/dev unoptimized. Tracked workspace [profile.dev] debug=0. No release gate recorded.",
"environment_provenance": "Root reports dot-sourcing .local/toolchain/env.ps1 for the five commands. The saved script is read as data, not executed during evidence preservation.",
"loaded_module_capture": false,
"linux": "Paused by explicit user instruction; no Linux/WSL/VPN work performed."
}
@@ -0,0 +1,128 @@
{
"captured_at_utc": "2026-09-11T17:21:31.8813243Z",
"commit": "847b1caa7e75bb82f6ce5193c45bcfc45973ad1a",
"branch": "codex/analysis-core-20260911",
"source_root": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\analysis-core-worktree-20260911",
"clean_before": true,
"expected_origin": "Nine source/dependency SHA256 values in independent codec source review and implementation freeze.",
"count": 9,
"inputs": [
{
"path": "Cargo.lock",
"expected_sha256": "19499D93DDE6F2E2A00AB4B7D5F37689DD7A7D492B2B5E23063A761607839FC5",
"committed_git_blob": "6a7ece82167ea28adde24b18916d63ac210b2303",
"working_raw_git_blob": "6a7ece82167ea28adde24b18916d63ac210b2303",
"matches_frozen": true,
"copy": {
"bytes": 120942,
"sha256": "19499D93DDE6F2E2A00AB4B7D5F37689DD7A7D492B2B5E23063A761607839FC5",
"source_after_sha256": "19499D93DDE6F2E2A00AB4B7D5F37689DD7A7D492B2B5E23063A761607839FC5",
"destination_sha256": "19499D93DDE6F2E2A00AB4B7D5F37689DD7A7D492B2B5E23063A761607839FC5"
}
},
{
"path": "crates/oa-core/Cargo.toml",
"expected_sha256": "3377D3D9D43FE0023AC09728B0D20205F86A6755F15518DC0CD035780B47A95D",
"committed_git_blob": "4f8e80edd17b890eeafd241cc5353095f5c0286c",
"working_raw_git_blob": "4f8e80edd17b890eeafd241cc5353095f5c0286c",
"matches_frozen": true,
"copy": {
"bytes": 465,
"sha256": "3377D3D9D43FE0023AC09728B0D20205F86A6755F15518DC0CD035780B47A95D",
"source_after_sha256": "3377D3D9D43FE0023AC09728B0D20205F86A6755F15518DC0CD035780B47A95D",
"destination_sha256": "3377D3D9D43FE0023AC09728B0D20205F86A6755F15518DC0CD035780B47A95D"
}
},
{
"path": "crates/oa-core/src/analysis.rs",
"expected_sha256": "845C3629098A13F3FFFAAC3E37D6A3CF8B7BDF33A8110465459990EB3FE70B11",
"committed_git_blob": "75dbdc21bf24aa6512c5aee951d989dd2cb62a9c",
"working_raw_git_blob": "75dbdc21bf24aa6512c5aee951d989dd2cb62a9c",
"matches_frozen": true,
"copy": {
"bytes": 2213,
"sha256": "845C3629098A13F3FFFAAC3E37D6A3CF8B7BDF33A8110465459990EB3FE70B11",
"source_after_sha256": "845C3629098A13F3FFFAAC3E37D6A3CF8B7BDF33A8110465459990EB3FE70B11",
"destination_sha256": "845C3629098A13F3FFFAAC3E37D6A3CF8B7BDF33A8110465459990EB3FE70B11"
}
},
{
"path": "crates/oa-core/src/analysis/types.rs",
"expected_sha256": "27363B1755F3C0943685A9837C69E010857811D4F67A0751B20EFDD9C1707AA3",
"committed_git_blob": "2c17e0cb4dddf4c52c37c703ee7278aa2140402f",
"working_raw_git_blob": "2c17e0cb4dddf4c52c37c703ee7278aa2140402f",
"matches_frozen": true,
"copy": {
"bytes": 5463,
"sha256": "27363B1755F3C0943685A9837C69E010857811D4F67A0751B20EFDD9C1707AA3",
"source_after_sha256": "27363B1755F3C0943685A9837C69E010857811D4F67A0751B20EFDD9C1707AA3",
"destination_sha256": "27363B1755F3C0943685A9837C69E010857811D4F67A0751B20EFDD9C1707AA3"
}
},
{
"path": "crates/oa-core/src/analysis/expression_lifecycle.rs",
"expected_sha256": "5D66E4E8F33FC8F90EC364251079617A36226E3EA68AC321ABAE74096E1C7C8C",
"committed_git_blob": "4c25bb24fb8b66a38b8428e0f88c2cd6a58a9c87",
"working_raw_git_blob": "4c25bb24fb8b66a38b8428e0f88c2cd6a58a9c87",
"matches_frozen": true,
"copy": {
"bytes": 13785,
"sha256": "5D66E4E8F33FC8F90EC364251079617A36226E3EA68AC321ABAE74096E1C7C8C",
"source_after_sha256": "5D66E4E8F33FC8F90EC364251079617A36226E3EA68AC321ABAE74096E1C7C8C",
"destination_sha256": "5D66E4E8F33FC8F90EC364251079617A36226E3EA68AC321ABAE74096E1C7C8C"
}
},
{
"path": "crates/oa-core/src/analysis/expression_serde.rs",
"expected_sha256": "581E4704FB4F2049E31C7731267BC9D52E3F17625B664C3D7C5F19CAA961C819",
"committed_git_blob": "02fdd03d8526bf833b024cae7a50b0607f801d8d",
"working_raw_git_blob": "02fdd03d8526bf833b024cae7a50b0607f801d8d",
"matches_frozen": true,
"copy": {
"bytes": 8388,
"sha256": "581E4704FB4F2049E31C7731267BC9D52E3F17625B664C3D7C5F19CAA961C819",
"source_after_sha256": "581E4704FB4F2049E31C7731267BC9D52E3F17625B664C3D7C5F19CAA961C819",
"destination_sha256": "581E4704FB4F2049E31C7731267BC9D52E3F17625B664C3D7C5F19CAA961C819"
}
},
{
"path": "crates/oa-core/src/persistence.rs",
"expected_sha256": "EDA5E697ADB29A8BF38B0D367099BD23267455E4792F97607E13DC0B232A3476",
"committed_git_blob": "574840108e6a1eb5e083cbe70a95b7bc1852716c",
"working_raw_git_blob": "574840108e6a1eb5e083cbe70a95b7bc1852716c",
"matches_frozen": true,
"copy": {
"bytes": 34055,
"sha256": "EDA5E697ADB29A8BF38B0D367099BD23267455E4792F97607E13DC0B232A3476",
"source_after_sha256": "EDA5E697ADB29A8BF38B0D367099BD23267455E4792F97607E13DC0B232A3476",
"destination_sha256": "EDA5E697ADB29A8BF38B0D367099BD23267455E4792F97607E13DC0B232A3476"
}
},
{
"path": "crates/oa-core/src/persistence/json_codec.rs",
"expected_sha256": "2AA2C52EEA794ACB85744440192DF9EF5193160AD6EBD1FEBAA824E144AADD90",
"committed_git_blob": "ff6c72831846b209b3c8600b447e335f68266d2d",
"working_raw_git_blob": "ff6c72831846b209b3c8600b447e335f68266d2d",
"matches_frozen": true,
"copy": {
"bytes": 14642,
"sha256": "2AA2C52EEA794ACB85744440192DF9EF5193160AD6EBD1FEBAA824E144AADD90",
"source_after_sha256": "2AA2C52EEA794ACB85744440192DF9EF5193160AD6EBD1FEBAA824E144AADD90",
"destination_sha256": "2AA2C52EEA794ACB85744440192DF9EF5193160AD6EBD1FEBAA824E144AADD90"
}
},
{
"path": "crates/oa-core/src/persistence/json_codec/tests.rs",
"expected_sha256": "7065CC9E177A5699432888110936529C621F13E7C4B164566AB1FD14B61D6B9C",
"committed_git_blob": "b49b5f95bba3f59e8befe17ab5fb688ee7d4e765",
"working_raw_git_blob": "b49b5f95bba3f59e8befe17ab5fb688ee7d4e765",
"matches_frozen": true,
"copy": {
"bytes": 21440,
"sha256": "7065CC9E177A5699432888110936529C621F13E7C4B164566AB1FD14B61D6B9C",
"source_after_sha256": "7065CC9E177A5699432888110936529C621F13E7C4B164566AB1FD14B61D6B9C",
"destination_sha256": "7065CC9E177A5699432888110936529C621F13E7C4B164566AB1FD14B61D6B9C"
}
}
]
}
@@ -0,0 +1,35 @@
Finished `test` profile [unoptimized] target(s) in 0.72s
Running unittests src\lib.rs (target\debug\deps\oa_core-73987e0d7089bf1e.exe)
running 28 tests
test analysis::admission::meter_tests::call_key_scan_and_argument_lookup_have_separate_precharges ... ok
test analysis::admission::meter_tests::relation_subject_resolution_charges_each_hop_and_bounds_cycles ... ok
test analysis::admission::meter_tests::static_meter_exact_boundary_zero_charge_and_exhaustion_latch ... ok
test analysis::admission::meter_tests::wide_expression_children_are_charged_before_they_are_queued ... ok
test analysis::tests::affine_definitions_require_linear_matching_difference_and_valid_units ... ok
test analysis::tests::anonymous_dimension_algebra_and_affine_rules_share_pure_runtime_helpers ... ok
test analysis::tests::calculations_are_pure_parameters_are_scoped_and_calls_bind_exactly ... ok
test analysis::tests::complete_real_model_roundtrips_and_exposes_explicit_effective_bindings ... ok
test analysis::tests::deletion_requires_explicit_repairs_in_the_same_atomic_batch ... ok
test analysis::tests::empty_lists_require_context_and_lists_are_flat_bounded_homogeneous ... ok
test analysis::tests::equal_dimensions_do_not_create_nominal_compatibility ... ok
test analysis::tests::every_case_is_checked_and_baseline_edits_cannot_introduce_a_case_only_cycle ... ok
test analysis::tests::existing_transport_record_limits_count_definitions_parameters_overrides_and_nodes ... ok
test analysis::tests::frozen_schema7_upgrades_without_interpreting_descriptive_metadata ... ok
test analysis::tests::global_reidentify_rewrites_typed_domains_but_preserves_scoped_parameters ... ok
test analysis::tests::layers_subject_kinds_and_display_units_are_strict ... ok
test analysis::tests::logical_depth_and_call_depth_have_exact_boundaries ... ok
test analysis::tests::map_key_collision_is_rejected_before_any_reference_mutation ... ok
test analysis::tests::narrow_override_commands_preserve_missing_clear_and_history ... ok
test analysis::tests::static_types_do_not_evaluate_domain_conditions_or_check_truth ... ok
test analysis::tests::static_work_is_lowerable_deterministic_and_aggregated_across_cases ... ok
test analysis::tests::stored_closure_limit_aggregates_distinct_called_definitions ... ok
test analysis::tests::unsupported_transition_and_header_only_review_fail_explicitly ... ok
test analysis::tests::unused_branches_still_reject_wrong_types_references_and_cycles ... ok
test analysis::tests::wire_contract_rejects_unknown_imports_duplicates_missing_fields_and_noncanonical_exact ... ok
test topology_context::analysis::tests::edge_work_budget_stops_repeated_expansion_not_just_output_storage ... ok
test topology_context::analysis::tests::exact_edge_budget_includes_reference_boundaries_and_node_abort_has_no_bypass ... ok
test topology_context::analysis::tests::unresolved_authored_edges_consume_work_budget_without_stored_output ... ok
test result: ok. 28 passed; 0 failed; 0 ignored; 0 measured; 71 filtered out; finished in 4.34s
@@ -0,0 +1,9 @@
Checking windows-sys v0.61.2
Checking psm v0.1.32
Checking winapi-util v0.1.11
Checking tempfile v3.27.0
Checking serde_json v1.0.151
Checking same-file v1.0.6
Checking stacker v0.1.25
Checking oa-core v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\analysis-core-worktree-20260911\crates\oa-core)
Finished `dev` profile [unoptimized] target(s) in 3m 13s
@@ -0,0 +1,33 @@
Finished `test` profile [unoptimized] target(s) in 0.91s
Running unittests src\lib.rs (target\debug\deps\oa_core-73987e0d7089bf1e.exe)
running 26 tests
test analysis_numeric::tests::affine_conversion_rejects_a_wider_stage_even_when_the_final_value_would_fit ... ok
test analysis_numeric::tests::affine_point_and_difference_arithmetic_has_distinct_kinds ... ok
test analysis_numeric::tests::anonymous_product_then_quotient_closes_and_preserves_scaling ... ok
test analysis_numeric::tests::arithmetic_matches_independent_small_integer_oracle ... ok
test analysis_numeric::tests::both_reduced_component_overflows_are_reported ... ok
test analysis_numeric::tests::c1_expanded_call_witness_terminates_at_dynamic_limit ... ok
test analysis_numeric::tests::canonical_wire_rejects_both_component_noncanonical_forms ... ok
test analysis_numeric::tests::cross_products_cancel_before_reduced_component_limit ... ok
test analysis_numeric::tests::decimals_are_exact_and_source_ratios_reduce ... ok
test analysis_numeric::tests::dimensions_use_checked_seven_axis_integer_algebra ... ok
test analysis_numeric::tests::duration_unit_coordinates_are_exact_and_nominal ... ok
test analysis_numeric::tests::exact_accounting_boundary_and_terminal_failure_are_stable ... ok
test analysis_numeric::tests::full_8193_bit_sum_intermediate_can_reduce_to_admitted_value ... ok
test analysis_numeric::tests::full_independent_r7_packets_round_trip_as_literals_and_wire ... ok
test analysis_numeric::tests::literal_reader_does_not_fold_expression_unary_minus ... ok
test analysis_numeric::tests::literal_syntax_and_decimal_exponent_are_bounded ... ok
test analysis_numeric::tests::multiple_targets_share_one_aggregate_meter ... ok
test analysis_numeric::tests::nominal_kinds_survive_comparison_but_explicit_quotients_can_erase_dimension ... ok
test analysis_numeric::tests::numeric_precharge_precedes_big_integer_result_allocation ... ok
test analysis_numeric::tests::numerical_power_types_and_dimension_overflow_are_checked ... ok
test analysis_numeric::tests::powers_and_signs_have_explicit_domains ... ok
test analysis_numeric::tests::provisional_limits_and_effective_limits_round_trip ... ok
test analysis_numeric::tests::r6_anonymous_compound_energy_needs_one_outer_assertion ... ok
test analysis_numeric::tests::serde_uses_closed_canonical_string_wire ... ok
test analysis_numeric::tests::wire_rejects_4097_bits_before_gcd_can_reduce_them ... ok
test analysis_numeric::tests::zero_cost_and_maximum_reservation_do_not_underflow_or_reactivate ... ok
test result: ok. 26 passed; 0 failed; 0 ignored; 0 measured; 73 filtered out; finished in 0.14s
@@ -0,0 +1,67 @@
Finished `test` profile [unoptimized] target(s) in 2.52s
Running unittests src\lib.rs (target\debug\deps\oa_core-73987e0d7089bf1e.exe)
running 6 tests
codec accepted: status=exit code: 0 elapsed=200.4702ms
running 1 test
test persistence::json_codec::tests::codec_lifecycle_subprocess ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 98 filtered out; finished in 0.15s
codec child phase=accepted, configured thread stack=131072
accepted call=false depth128 input=5027 output=5028 tokens=1697 entries=423 raw strings=2760 containers=261 debug=4464
accepted call=true depth128 input=16838 output=16839 tokens=2713 entries=677 raw strings=13301 containers=388 debug=17418
codec refused: status=exit code: 0 elapsed=2.6268323s
running 1 test
test persistence::json_codec::tests::codec_lifecycle_subprocess ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 98 filtered out; finished in 2.54s
codec child phase=refused, configured thread stack=131072
codec malformed: status=exit code: 0 elapsed=351.4043ms
running 1 test
test persistence::json_codec::tests::codec_lifecycle_subprocess ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 98 filtered out; finished in 0.09s
codec child phase=malformed, configured thread stack=131072
duplicate operand: Invalid project JSON: duplicate field `operand` at line 1 column 4954
escaped duplicate: Invalid project JSON: duplicate field `operand` at line 1 column 4959
unknown field: Invalid project JSON: unknown field `surprise`, expected `op` or `operand` at line 1 column 4955
duplicate enum: Invalid project JSON: expected value at line 1 column 4945
duplicate parameter: Invalid project JSON: Duplicate parameter ID 00000000-0000-0000-0000-000000000002 at line 1 column 5080
codec ownership: status=exit code: 0 elapsed=2.0802348s
running 1 test
test persistence::json_codec::tests::codec_lifecycle_subprocess ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 98 filtered out; finished in 1.91s
codec child phase=ownership, configured thread stack=131072
iterative manual100000 ownership/debug/drop completed; debug bytes=2799985
codec atomic: status=exit code: 0 elapsed=281.9043ms
running 1 test
test persistence::json_codec::tests::codec_lifecycle_subprocess ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 98 filtered out; finished in 0.15s
codec child phase=atomic, configured thread stack=131072
test persistence::json_codec::tests::codec_lifecycle_subprocess ... ok
JSON profile: graph bytes=17589111, ordered-reference bytes=19500001, max container entries=500000
test persistence::json_codec::tests::json_profile_does_not_contract_declared_cli_collection_counts ... ok
test persistence::json_codec::tests::production_save_always_reopens_with_its_reader_profile ... ok
test persistence::json_codec::tests::remote_serde_keeps_each_existing_expression_variant ... ok
test persistence::json_codec::tests::resource_preflight_has_exact_lowerable_boundaries ... ok
test persistence::json_codec::tests::scanner_is_not_a_parallel_json_grammar ... ok
test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 93 filtered out; finished in 36.65s
@@ -0,0 +1,71 @@
Compiling object v0.39.1
Compiling windows-sys v0.61.2
Compiling ar_archive_writer v0.5.3
Compiling psm v0.1.32
Compiling stacker v0.1.25
Compiling serde_json v1.0.151
Compiling winapi-util v0.1.11
Compiling tempfile v3.27.0
Compiling same-file v1.0.6
Compiling oa-core v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\analysis-core-worktree-20260911\crates\oa-core)
Finished `test` profile [unoptimized] target(s) in 19m 16s
Running unittests src\lib.rs (target\debug\deps\oa_core-73987e0d7089bf1e.exe)
running 1 test
codec accepted: status=exit code: 0 elapsed=224.68ms
running 1 test
test persistence::json_codec::tests::codec_lifecycle_subprocess ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 98 filtered out; finished in 0.13s
codec child phase=accepted, configured thread stack=131072
accepted call=false depth128 input=5027 output=5028 tokens=1697 entries=423 raw strings=2760 containers=261 debug=4464
accepted call=true depth128 input=16838 output=16839 tokens=2713 entries=677 raw strings=13301 containers=388 debug=17418
codec refused: status=exit code: 0 elapsed=2.3448787s
running 1 test
test persistence::json_codec::tests::codec_lifecycle_subprocess ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 98 filtered out; finished in 2.24s
codec child phase=refused, configured thread stack=131072
codec malformed: status=exit code: 0 elapsed=458.7141ms
running 1 test
test persistence::json_codec::tests::codec_lifecycle_subprocess ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 98 filtered out; finished in 0.35s
codec child phase=malformed, configured thread stack=131072
duplicate operand: Invalid project JSON: duplicate field `operand` at line 1 column 4954
escaped duplicate: Invalid project JSON: duplicate field `operand` at line 1 column 4959
unknown field: Invalid project JSON: unknown field `surprise`, expected `op` or `operand` at line 1 column 4955
duplicate enum: Invalid project JSON: expected value at line 1 column 4945
duplicate parameter: Invalid project JSON: Duplicate parameter ID 00000000-0000-0000-0000-000000000002 at line 1 column 5080
codec ownership: status=exit code: 0 elapsed=1.6468743s
running 1 test
test persistence::json_codec::tests::codec_lifecycle_subprocess ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 98 filtered out; finished in 1.39s
codec child phase=ownership, configured thread stack=131072
iterative manual100000 ownership/debug/drop completed; debug bytes=2799985
codec atomic: status=exit code: 0 elapsed=1.1414496s
running 1 test
test persistence::json_codec::tests::codec_lifecycle_subprocess ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 98 filtered out; finished in 0.64s
codec child phase=atomic, configured thread stack=131072
test persistence::json_codec::tests::codec_lifecycle_subprocess ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 98 filtered out; finished in 6.08s
@@ -0,0 +1,203 @@
{
"format_version": 1,
"captured_at_utc": "2026-09-11T17:21:39.7035692Z",
"author": "Codex-956atf /root/analysis_core_resume",
"purpose": "Immutable preservation of closed root-run Windows debug codec gates; no gate rerun, source edit, Git/index mutation or Linux action.",
"source_commit": "847b1caa7e75bb82f6ce5193c45bcfc45973ad1a",
"source_branch": "codex/analysis-core-20260911",
"source_root": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\analysis-core-worktree-20260911",
"source_clean_before_and_after": true,
"local_archive": ".local/analysis-codec-windows-gate-20260911",
"durable_archive": "harness/experiments/analysis-codec-windows-20260911",
"frozen_input_count": 9,
"tracked_core_input_count": 122,
"metadata": [
{
"path": "binary-snapshot.json",
"bytes": 1984,
"sha256": "B8811E16FD5FF14020F231F19777D6C0AFAAE432821FBB63E0919B2CE1845509"
},
{
"path": "pe-imports.json",
"bytes": 612,
"sha256": "5F0D1D5E3234A6FC910790270719247647849300CE14C911F412897E454289C1"
},
{
"path": "frozen-inputs.json",
"bytes": 6436,
"sha256": "D48994D14CB6D1D128E52FEE32CC792E348DC0295C7AE8B1B87F31C088575F03"
},
{
"path": "tracked-core-inputs.json",
"bytes": 21784,
"sha256": "056BB245361A0958DA0D5A8054A91EA5C225B68104AB17BAD132CD331D508A21"
},
{
"path": "closed-logs.json",
"bytes": 6572,
"sha256": "E7D5176C71895DB316C5D10AF4A15759E569759E2AF080317C706440C1EBD709"
},
{
"path": "context.json",
"bytes": 1544,
"sha256": "D0F1802BE7D43EB33BFE1A4CAB087C58571605E622BCD4349925A4310F3C9CA6"
}
],
"runtime_payloads": [
{
"source": "target/debug/deps/oa_core-73987e0d7089bf1e.exe",
"destination": "binary/oa_core-73987e0d7089bf1e.exe",
"bytes": 34962432,
"sha256": "1B7E020F35312FB8ACB163ED1A9C29890CDD4379C03E238D47BA46575E35CC6F",
"source_after_sha256": "1B7E020F35312FB8ACB163ED1A9C29890CDD4379C03E238D47BA46575E35CC6F",
"destination_sha256": "1B7E020F35312FB8ACB163ED1A9C29890CDD4379C03E238D47BA46575E35CC6F"
},
{
"source": ".local/toolchain/llvm-mingw-20260908-ucrt-x86_64/bin/libunwind.dll",
"destination": "binary/libunwind.dll",
"bytes": 190464,
"sha256": "AA282FA47BB2C4E210CEA5E470CDBDCA58568F0BE0BC73E404D86AB9CD95EF6A",
"source_after_sha256": "AA282FA47BB2C4E210CEA5E470CDBDCA58568F0BE0BC73E404D86AB9CD95EF6A",
"destination_sha256": "AA282FA47BB2C4E210CEA5E470CDBDCA58568F0BE0BC73E404D86AB9CD95EF6A"
},
{
"source": ".local/toolchain/rustup/toolchains/stable-x86_64-pc-windows-gnullvm/bin/libunwind.dll",
"destination": "runtime-alternates/rust-stable/libunwind.dll",
"bytes": 90624,
"sha256": "13BF4E99B0193634EBDEB0BBDEFF9753B39F1D04799183F664111014AABF4C2C",
"source_after_sha256": "13BF4E99B0193634EBDEB0BBDEFF9753B39F1D04799183F664111014AABF4C2C",
"destination_sha256": "13BF4E99B0193634EBDEB0BBDEFF9753B39F1D04799183F664111014AABF4C2C"
}
],
"closed_logs": [
{
"name": "analysis-codec-subprocess-first-20260911.log",
"source": ".local/logs/analysis-codec-subprocess-first-20260911.log",
"source_last_write_utc": "2026-09-11T16:48:01.0537466Z",
"command": "cargo test --locked -p oa-core --lib persistence::json_codec::tests::codec_lifecycle_subprocess -- --exact --test-threads=1 --nocapture",
"terminal_exit_code": 0,
"exit_code_provenance": "Root-reported terminal result; raw log has no shell status trailer.",
"observed": "Driver 1 passed, 98 filtered; accepted/refused/malformed/ownership/atomic children each exit code 0 on configured 131072-byte threads.",
"elapsed": "Compile 19m16s; driver 6.08s",
"carriage_return_bytes": 71,
"local_copy": {
"bytes": 3104,
"sha256": "BCD053C7226E17171BF127BE8B3A27A2F75ED13516674AE2DF78D09F5353A21F",
"source_after_sha256": "BCD053C7226E17171BF127BE8B3A27A2F75ED13516674AE2DF78D09F5353A21F",
"destination_sha256": "BCD053C7226E17171BF127BE8B3A27A2F75ED13516674AE2DF78D09F5353A21F"
},
"durable_copy": {
"bytes": 3104,
"sha256": "BCD053C7226E17171BF127BE8B3A27A2F75ED13516674AE2DF78D09F5353A21F",
"source_after_sha256": "BCD053C7226E17171BF127BE8B3A27A2F75ED13516674AE2DF78D09F5353A21F",
"destination_sha256": "BCD053C7226E17171BF127BE8B3A27A2F75ED13516674AE2DF78D09F5353A21F"
}
},
{
"name": "analysis-codec-six-20260911.log",
"source": ".local/logs/analysis-codec-six-20260911.log",
"source_last_write_utc": "2026-09-11T16:51:35.2692799Z",
"command": "cargo test --locked -p oa-core --lib persistence::json_codec::tests -- --test-threads=1 --nocapture",
"terminal_exit_code": 0,
"exit_code_provenance": "Root-reported terminal result; raw log has no shell status trailer.",
"observed": "6 passed, 93 filtered; five subprocess child phases each exit code 0.",
"elapsed": "Compile/cache finish 2.52s; tests 36.65s",
"carriage_return_bytes": 67,
"local_copy": {
"bytes": 3273,
"sha256": "20DAC6C5C8FEE3FC60EA8C2733EEB7EA438055B08B59D9F8F8C81DE9D72F5EF8",
"source_after_sha256": "20DAC6C5C8FEE3FC60EA8C2733EEB7EA438055B08B59D9F8F8C81DE9D72F5EF8",
"destination_sha256": "20DAC6C5C8FEE3FC60EA8C2733EEB7EA438055B08B59D9F8F8C81DE9D72F5EF8"
},
"durable_copy": {
"bytes": 3273,
"sha256": "20DAC6C5C8FEE3FC60EA8C2733EEB7EA438055B08B59D9F8F8C81DE9D72F5EF8",
"source_after_sha256": "20DAC6C5C8FEE3FC60EA8C2733EEB7EA438055B08B59D9F8F8C81DE9D72F5EF8",
"destination_sha256": "20DAC6C5C8FEE3FC60EA8C2733EEB7EA438055B08B59D9F8F8C81DE9D72F5EF8"
}
},
{
"name": "analysis-codec-analysis28-20260911.log",
"source": ".local/logs/analysis-codec-analysis28-20260911.log",
"source_last_write_utc": "2026-09-11T16:51:40.6379426Z",
"command": "cargo test --locked -p oa-core --lib analysis:: -- --test-threads=1",
"terminal_exit_code": 0,
"exit_code_provenance": "Root-reported terminal result; raw log has no shell status trailer.",
"observed": "28 passed, 71 filtered; 25 schema8 tests plus 3 existing topology_context::analysis tests.",
"elapsed": "Compile/cache finish 0.72s; tests 4.34s",
"carriage_return_bytes": 35,
"local_copy": {
"bytes": 3062,
"sha256": "26FF62565CDA9F2AD64DB31C0634593A052E9BFE03577B641E44AAD2EA8129DF",
"source_after_sha256": "26FF62565CDA9F2AD64DB31C0634593A052E9BFE03577B641E44AAD2EA8129DF",
"destination_sha256": "26FF62565CDA9F2AD64DB31C0634593A052E9BFE03577B641E44AAD2EA8129DF"
},
"durable_copy": {
"bytes": 3062,
"sha256": "26FF62565CDA9F2AD64DB31C0634593A052E9BFE03577B641E44AAD2EA8129DF",
"source_after_sha256": "26FF62565CDA9F2AD64DB31C0634593A052E9BFE03577B641E44AAD2EA8129DF",
"destination_sha256": "26FF62565CDA9F2AD64DB31C0634593A052E9BFE03577B641E44AAD2EA8129DF"
}
},
{
"name": "analysis-codec-numeric26-20260911.log",
"source": ".local/logs/analysis-codec-numeric26-20260911.log",
"source_last_write_utc": "2026-09-11T16:51:41.9775469Z",
"command": "cargo test --locked -p oa-core --lib analysis_numeric::tests -- --test-threads=1",
"terminal_exit_code": 0,
"exit_code_provenance": "Root-reported terminal result; raw log has no shell status trailer.",
"observed": "26 passed, 73 filtered.",
"elapsed": "Compile/cache finish 0.91s; tests 0.14s",
"carriage_return_bytes": 33,
"local_copy": {
"bytes": 2664,
"sha256": "7B5A1B7B7F52CE135F0E406D8E09A092DD0E93AEDD07DF42B2D8776E1D4DA33E",
"source_after_sha256": "7B5A1B7B7F52CE135F0E406D8E09A092DD0E93AEDD07DF42B2D8776E1D4DA33E",
"destination_sha256": "7B5A1B7B7F52CE135F0E406D8E09A092DD0E93AEDD07DF42B2D8776E1D4DA33E"
},
"durable_copy": {
"bytes": 2664,
"sha256": "7B5A1B7B7F52CE135F0E406D8E09A092DD0E93AEDD07DF42B2D8776E1D4DA33E",
"source_after_sha256": "7B5A1B7B7F52CE135F0E406D8E09A092DD0E93AEDD07DF42B2D8776E1D4DA33E",
"destination_sha256": "7B5A1B7B7F52CE135F0E406D8E09A092DD0E93AEDD07DF42B2D8776E1D4DA33E"
}
},
{
"name": "analysis-codec-clippy-20260911.log",
"source": ".local/logs/analysis-codec-clippy-20260911.log",
"source_last_write_utc": "2026-09-11T16:54:56.0732308Z",
"command": "cargo clippy --locked -p oa-core --all-targets -- -D warnings",
"terminal_exit_code": 0,
"exit_code_provenance": "Root-reported terminal result; raw log has no shell status trailer.",
"observed": "Finished dev profile [unoptimized]; no warnings or errors in the closed log.",
"elapsed": "3m13s",
"carriage_return_bytes": 9,
"local_copy": {
"bytes": 410,
"sha256": "3FFF0B9934BECDF65707CB8EDBF5F85FE2C2E1C8053C71EEAE5B3D32494E6218",
"source_after_sha256": "3FFF0B9934BECDF65707CB8EDBF5F85FE2C2E1C8053C71EEAE5B3D32494E6218",
"destination_sha256": "3FFF0B9934BECDF65707CB8EDBF5F85FE2C2E1C8053C71EEAE5B3D32494E6218"
},
"durable_copy": {
"bytes": 410,
"sha256": "3FFF0B9934BECDF65707CB8EDBF5F85FE2C2E1C8053C71EEAE5B3D32494E6218",
"source_after_sha256": "3FFF0B9934BECDF65707CB8EDBF5F85FE2C2E1C8053C71EEAE5B3D32494E6218",
"destination_sha256": "3FFF0B9934BECDF65707CB8EDBF5F85FE2C2E1C8053C71EEAE5B3D32494E6218"
}
}
],
"binary_binding": "Closed logs name the isolated oa-core source and oa_core-73987e0d7089bf1e.exe. Current binary was preserved before further root compiler work; source hashes were checked after gates against clean commit 847b1caa.",
"runtime_binding": "Root reports every gate dot-sourced .local/toolchain/env.ps1, which prepended CARGO_HOME/bin then LLVM-MinGW bin. Environment-path libunwind is staged beside binary; identical current stable host/target runtime is separately preserved as an alternative. Cargo may add search paths. No loaded-module capture was taken, so no definitive loaded DLL binding is claimed.",
"runtime_alternative": "Current Rust stable host bin and target rustlib/bin libunwind.dll both have SHA256 13BF4E99B0193634EBDEB0BBDEFF9753B39F1D04799183F664111014AABF4C2C (90624 bytes); the environment-path LLVM-MinGW candidate is AA282FA47BB2C4E210CEA5E470CDBDCA58568F0BE0BC73E404D86AB9CD95EF6A (190464 bytes).",
"verification": "Source-before/source-after/copied SHA256 and byte lengths agree. Durable copies preserve original closed-log bytes; metadata uses UTF-8 LF. See readback.json for the final independent reread.",
"production_depth_128_validated": false,
"remaining": [
"Frozen legacy/migration/checked-save regression integrations",
"Windows release execution and measured peak memory/time under broad accepted and refused shapes",
"Deep partial legacy Value teardown and mixed Expr variant/width lifecycle closure",
"Large combined ordinary/historical Project shapes; counts are not proof of acceptable memory or UI speed",
"Source8 grammar and source parity; server/CLI/command/response adapters and their output/error caps",
"Evaluator and final product acceptance",
"Linux runtime acceptance: explicitly deferred by user; do not auto-resume"
]
}
@@ -0,0 +1,21 @@
{
"method": "Read PE import directory bytes; executable not launched",
"machine": 34404,
"optional_header_magic": 523,
"imports": [
"KERNEL32.dll",
"bcryptprimitives.dll",
"libunwind.dll",
"api-ms-win-crt-stdio-l1-1-0.dll",
"api-ms-win-crt-runtime-l1-1-0.dll",
"api-ms-win-crt-locale-l1-1-0.dll",
"api-ms-win-crt-math-l1-1-0.dll",
"api-ms-win-crt-heap-l1-1-0.dll",
"api-ms-win-crt-private-l1-1-0.dll",
"api-ms-win-crt-string-l1-1-0.dll",
"api-ms-win-core-synch-l1-2-0.dll",
"ntdll.dll",
"USERENV.dll",
"api-ms-win-crt-environment-l1-1-0.dll"
]
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,619 @@
{
"captured_at_utc": "2026-09-11T17:21:34.9984458Z",
"commit": "847b1caa7e75bb82f6ce5193c45bcfc45973ad1a",
"selection": "Read-only git ls-files -- Cargo.toml Cargo.lock rust-toolchain* .cargo crates/oa-core",
"scope": "Post-gate raw hashes of tracked workspace manifests and oa-core files, including fixtures. This is a source snapshot, not a captured compiler input trace.",
"count": 122,
"files": [
{
"path": "Cargo.lock",
"bytes": 120942,
"sha256": "19499D93DDE6F2E2A00AB4B7D5F37689DD7A7D492B2B5E23063A761607839FC5"
},
{
"path": "Cargo.toml",
"bytes": 400,
"sha256": "369F5B4B2B60705A94623C8C0B68F8E0B048F263280CE07AE0629AD353797D62"
},
{
"path": "crates/oa-core/Cargo.toml",
"bytes": 465,
"sha256": "3377D3D9D43FE0023AC09728B0D20205F86A6755F15518DC0CD035780B47A95D"
},
{
"path": "crates/oa-core/build.rs",
"bytes": 45,
"sha256": "AB1BE6AE1FD8BF3EBAEEF6348347A9D5E666BDE693126F199E041659E0B094A9"
},
{
"path": "crates/oa-core/examples/generate_demo.rs",
"bytes": 795,
"sha256": "E26F432C95331AECA50985DBB3AD2C558AFFD43023B585811F0F7BFAA59F6661"
},
{
"path": "crates/oa-core/examples/generate_release1.rs",
"bytes": 1798,
"sha256": "1D5F11165D74893E4BC18DBF3A2F2111F4026BD89FD06EE7F0BAE69E45C4D4B5"
},
{
"path": "crates/oa-core/examples/release_dense_views.rs",
"bytes": 14664,
"sha256": "A83158488EF9B7394F136C52B1496719BE935C4E5BDFE457CAB5C9894F5F9412"
},
{
"path": "crates/oa-core/examples/routing_workshop.rs",
"bytes": 17248,
"sha256": "571307B18B6D4FA04700C5A4D4B0C2154C419E93A21292B2028E7B6F46A2FAA7"
},
{
"path": "crates/oa-core/src/analysis.rs",
"bytes": 2213,
"sha256": "845C3629098A13F3FFFAAC3E37D6A3CF8B7BDF33A8110465459990EB3FE70B11"
},
{
"path": "crates/oa-core/src/analysis/admission.rs",
"bytes": 39476,
"sha256": "5F85EAA868AC43C4819C98556281D8226F9C0E2A4B11E5C97CF24D855D2127AE"
},
{
"path": "crates/oa-core/src/analysis/expression_lifecycle.rs",
"bytes": 13785,
"sha256": "5D66E4E8F33FC8F90EC364251079617A36226E3EA68AC321ABAE74096E1C7C8C"
},
{
"path": "crates/oa-core/src/analysis/expression_serde.rs",
"bytes": 8388,
"sha256": "581E4704FB4F2049E31C7731267BC9D52E3F17625B664C3D7C5F19CAA961C819"
},
{
"path": "crates/oa-core/src/analysis/references.rs",
"bytes": 8757,
"sha256": "FE50EDC049B9900D91CADE3F58D15ACA4101211C39BB090317F548259D634529"
},
{
"path": "crates/oa-core/src/analysis/resolve.rs",
"bytes": 5222,
"sha256": "67AAA97DA8190DEDE71F0E2F98F1884A1DA38596B4B2DC6F8D61FF4377BDC80F"
},
{
"path": "crates/oa-core/src/analysis/tests.rs",
"bytes": 44413,
"sha256": "9FB30B28E305CB00E7C39204912F8311E96AA92D0334CE4A73FB08FADDFE83C5"
},
{
"path": "crates/oa-core/src/analysis/typecheck.rs",
"bytes": 18244,
"sha256": "831F14346BBAE83FBDEEB744F9F29460FAB13A8315645FAA6CE94D0B60B1516F"
},
{
"path": "crates/oa-core/src/analysis/types.rs",
"bytes": 5463,
"sha256": "27363B1755F3C0943685A9837C69E010857811D4F67A0751B20EFDD9C1707AA3"
},
{
"path": "crates/oa-core/src/analysis_numeric.rs",
"bytes": 3735,
"sha256": "6C88EEF3119162AEB6C07B9E75812C3AF9FBFE75554B0503AD00F103CB478408"
},
{
"path": "crates/oa-core/src/analysis_numeric/dimension.rs",
"bytes": 3045,
"sha256": "D4AD8E5307A1620E31791D724A0E31FDBA860D00DC83E3E1F2209D9C5E22A340"
},
{
"path": "crates/oa-core/src/analysis_numeric/exact.rs",
"bytes": 17338,
"sha256": "4EB8CB0F66B10E5DA204899AF82EE26ADFEC74BAB47B1A6C9842B5488EE04C4D"
},
{
"path": "crates/oa-core/src/analysis_numeric/fixtures/r7-literal-boundaries.json",
"bytes": 6085,
"sha256": "3B959DEC5E100A6F03817F5314BEF497347BA579EBD235476516D4BD83399181"
},
{
"path": "crates/oa-core/src/analysis_numeric/meter.rs",
"bytes": 7129,
"sha256": "1F7174BEB992935B4C399ACDEF91114AF73FAF041070FEAB968455AC827E8EE4"
},
{
"path": "crates/oa-core/src/analysis_numeric/quantity.rs",
"bytes": 12677,
"sha256": "B9F23A9485F8BD913722023C35FB9D8917C9F160078655B3A5C52C8F7F12AB94"
},
{
"path": "crates/oa-core/src/analysis_numeric/tests.rs",
"bytes": 27486,
"sha256": "323CE82FC057D36F44463569FD8EA324C7D700FDDFBF32BCE15E04BD2A66970B"
},
{
"path": "crates/oa-core/src/data.rs",
"bytes": 42696,
"sha256": "7F9C7C7DD8353CB5A38597B92CDE6D3E890203D392473187EB387D41FF37414A"
},
{
"path": "crates/oa-core/src/demo.rs",
"bytes": 20657,
"sha256": "AD62123F96C20FF023CAFC1A9849B0FA06BC62EDD1FB9701799E4599B458600C"
},
{
"path": "crates/oa-core/src/language.rs",
"bytes": 100419,
"sha256": "B65098321ECF79E4DC1182A7FAB7BAFEEC0A238BB56153E81782F04FAB838558"
},
{
"path": "crates/oa-core/src/language/semantic_source.rs",
"bytes": 7326,
"sha256": "584B5BB59D87A7B8D44D5DD2D124C3A1323B21492B138A17BC9526C0087C9E29"
},
{
"path": "crates/oa-core/src/language_nesting_tests.rs",
"bytes": 20639,
"sha256": "BD061611E8CB6F28AB203C73E544AC98BF275EB464CD314AD5FBD25F773DF33B"
},
{
"path": "crates/oa-core/src/lib.rs",
"bytes": 567,
"sha256": "C9BF3462A57A465B6CC7ECD7866E0D331B35F299E89D9C917E0A4587BB1DCD89"
},
{
"path": "crates/oa-core/src/migration_v2/README.md",
"bytes": 1515,
"sha256": "FB61D3FAA33D7BDFBA73415415A0882C0FBD3E0B45DF42F972A19AEC6DA927B9"
},
{
"path": "crates/oa-core/src/migration_v2/adapter.rs",
"bytes": 9490,
"sha256": "66741B4C8354A23B8A932ECC5BF1BC3BB6E74279558757B8C35BC2157A7E51B5"
},
{
"path": "crates/oa-core/src/migration_v2/mod.rs",
"bytes": 1158,
"sha256": "857D8BB13B6814DABA6780CA770C16352943C01CB130245D055F11B3B957B3E9"
},
{
"path": "crates/oa-core/src/migration_v2/model.rs",
"bytes": 52059,
"sha256": "8AC63EB59B2E57BDECFF2477EE0634096090924D3DD1851E1C454880A889DACB"
},
{
"path": "crates/oa-core/src/migration_v2/references.rs",
"bytes": 13369,
"sha256": "33A6819ABC83139FDAEBEEBD1B553238A328637106CCE7DD5A535F1791D43E20"
},
{
"path": "crates/oa-core/src/migration_v2/source.rs",
"bytes": 21946,
"sha256": "74FEE9B24E7C912182C998AB9D9AEEC125E2009C3214959229E844C67AFE2BF6"
},
{
"path": "crates/oa-core/src/migration_v2/state_validation.rs",
"bytes": 15769,
"sha256": "E29EF071BAB0A68460DDF55C46131C6F66C022D52C3C602C37C013C3649DA1B0"
},
{
"path": "crates/oa-core/src/migration_v2/transitions.rs",
"bytes": 704,
"sha256": "F74174C532BA56B57C80B719A11FB4ADEA0A78CCC2828FFF72FE2B9EDA5A10B1"
},
{
"path": "crates/oa-core/src/migration_v2/validation.rs",
"bytes": 98736,
"sha256": "6D5E4A4E2805AE5045BF789A0485E14A61C63346587BACE2B4A91173A688F780"
},
{
"path": "crates/oa-core/src/migration_v3/README.md",
"bytes": 2128,
"sha256": "3755A63F840D7F0EADF68AFA7A9D587877A9468FF982D3610539E8A0BB067A82"
},
{
"path": "crates/oa-core/src/migration_v3/mod.rs",
"bytes": 23023,
"sha256": "F4EDB6A0B50ECFE8670973776D703047B064420CB1F05913129C00FC656B3722"
},
{
"path": "crates/oa-core/src/migration_v4.rs",
"bytes": 4062,
"sha256": "BF4056DA11AC4F27627320742539CC2408ED42334106F3068C432FF70C62DB25"
},
{
"path": "crates/oa-core/src/migration_v7.rs",
"bytes": 2097,
"sha256": "474F4CEBDD343A7EECDF243149BBCBF08E12EAA05B8532BF2593705287017B30"
},
{
"path": "crates/oa-core/src/model.rs",
"bytes": 68092,
"sha256": "1C06BCC4AA7ABFEF89876C3A001171C742558D664612CF36152AC0AA52BC7BAF"
},
{
"path": "crates/oa-core/src/operations.rs",
"bytes": 11405,
"sha256": "A5FFBBF54CD440316A106D1188BFB699B57DAE59F466EDCB33618C333CD19F41"
},
{
"path": "crates/oa-core/src/persistence.rs",
"bytes": 34055,
"sha256": "EDA5E697ADB29A8BF38B0D367099BD23267455E4792F97607E13DC0B232A3476"
},
{
"path": "crates/oa-core/src/persistence/json_codec.rs",
"bytes": 14642,
"sha256": "2AA2C52EEA794ACB85744440192DF9EF5193160AD6EBD1FEBAA824E144AADD90"
},
{
"path": "crates/oa-core/src/persistence/json_codec/tests.rs",
"bytes": 21440,
"sha256": "7065CC9E177A5699432888110936529C621F13E7C4B164566AB1FD14B61D6B9C"
},
{
"path": "crates/oa-core/src/ports.rs",
"bytes": 45413,
"sha256": "5CAC12EEA70A54EBEBBD6BE77B63CE92E3195325C4BC7D27705C518686CBAC80"
},
{
"path": "crates/oa-core/src/projection.rs",
"bytes": 21374,
"sha256": "69ED12BACE2CC5024E151752A174AB14034AC2D05EB741FCF13E9A27521C059B"
},
{
"path": "crates/oa-core/src/projection/port_layout.rs",
"bytes": 13474,
"sha256": "A08D17D10A58BB8B206DD001B19B91D5AA3A9F743B298A4DA57D503223C20079"
},
{
"path": "crates/oa-core/src/projection/state.rs",
"bytes": 5233,
"sha256": "73A607BAC713EC7FF49A509FEFB1A53C7686DCA93EE241FFC835564D743AFB28"
},
{
"path": "crates/oa-core/src/references.rs",
"bytes": 22840,
"sha256": "63CD8655F67D919CC8F6B796F4CEC42D7B020AA37880E50F19190AAB8B0029A3"
},
{
"path": "crates/oa-core/src/routing.rs",
"bytes": 50309,
"sha256": "C5EBB350BA76F731F5C48D968BFBBA4B6EF68EC046EB5893D116C4637EF15080"
},
{
"path": "crates/oa-core/src/semantic.rs",
"bytes": 540,
"sha256": "F9895DD5B8FB27D10D4CFBB41C3542EF2018365A589B8D5778E5674AE5A1F239"
},
{
"path": "crates/oa-core/src/semantic/catalog.rs",
"bytes": 49642,
"sha256": "47D74ACC4862139BA835DB3D6B769411E27860A99426A7F03EDEA26F44F7F7B3"
},
{
"path": "crates/oa-core/src/semantic/historical.rs",
"bytes": 15271,
"sha256": "EAF3284BF234312698514CDB636BC8348E1B5D746CB16B18DF777E77D312ABFB"
},
{
"path": "crates/oa-core/src/semantic/linked.rs",
"bytes": 6035,
"sha256": "6F335B14E809AECFC35DEFCAF77CB821B080D1506DBC46297912B25AF98C07A2"
},
{
"path": "crates/oa-core/src/semantic/mutate.rs",
"bytes": 22683,
"sha256": "DC2B4C4A6EB156FE468EAA8C85FD37A505C0CD8EA1F226CB69CD01689AFD3D9D"
},
{
"path": "crates/oa-core/src/semantic/review.rs",
"bytes": 51116,
"sha256": "0C157D2C327E72E89CCA3B423E71EE73798105C5BB7271439C1A101642C91E3E"
},
{
"path": "crates/oa-core/src/semantic/types.rs",
"bytes": 10405,
"sha256": "6922FE35089F12FAD43AB0CE1C6886D01A3855F90B8E2543E99802AC5E6F1BBB"
},
{
"path": "crates/oa-core/src/session.rs",
"bytes": 38211,
"sha256": "06115011BC86F0C95BBBE6D14BDAEDD7B83A66C0104DB186900B0F047B0F9C43"
},
{
"path": "crates/oa-core/src/source_files.rs",
"bytes": 74803,
"sha256": "669491DBDDB8E8E6556161426DFFA1A49AC1C826D31020BFF0956D81A4D84683"
},
{
"path": "crates/oa-core/src/source_project.rs",
"bytes": 61843,
"sha256": "1394370BCC5908873B830C2C11312E6E1611AF41F46C4F43CA0A6BCB713BB466"
},
{
"path": "crates/oa-core/src/source_project/source_migration.rs",
"bytes": 4700,
"sha256": "ADD463AE85C420F853D324B00A221F0F874164CC3F3ED6BBB07557A9ED7A8C95"
},
{
"path": "crates/oa-core/src/source_project/source_structure.rs",
"bytes": 20225,
"sha256": "9F1F83315856A7025FE662C0FBB13452F31613224A03663150D6225D1DDC162F"
},
{
"path": "crates/oa-core/src/state_validation.rs",
"bytes": 15665,
"sha256": "E28D7F74385E2A78763716B34491BD73A00BFECCC085970CEB130627E3FAEE18"
},
{
"path": "crates/oa-core/src/topology_context.rs",
"bytes": 13734,
"sha256": "16DE69B297DE95D93C73808621607833B713AF811DD082F0169186AE1E5C3E2D"
},
{
"path": "crates/oa-core/src/topology_context/analysis.rs",
"bytes": 20036,
"sha256": "DEAD6194F0AA60CC46045C0E5EE10DBDBB689158C38634DFBBBC45D2BA88AC65"
},
{
"path": "crates/oa-core/src/topology_context/analysis_tests.rs",
"bytes": 5258,
"sha256": "17839263410815E96FE027D4EBA3AAE0A9AB3B82B16947E2B29EBE0D09782B56"
},
{
"path": "crates/oa-core/src/transitions.rs",
"bytes": 52718,
"sha256": "127E429BCA4B2EA9A189ECE7E7B6DFD05A30B5FDEE98B2D10288A6F7C17B5931"
},
{
"path": "crates/oa-core/src/transitions/communication.rs",
"bytes": 8634,
"sha256": "2B476630A88EDD9021B9156CA12A5402E01604095F0D4002AFB452671903A028"
},
{
"path": "crates/oa-core/src/validation.rs",
"bytes": 113388,
"sha256": "E4C61F5ABB22AE3E595E16E93472E4364CDE4DB9064EC6DF826228C3D5962E42"
},
{
"path": "crates/oa-core/src/validation/coverage.rs",
"bytes": 6478,
"sha256": "B08E4B344ECAD055714168C83BEFF7FE29BA4EDC53EDBFFA2EAE04E5580A4D5F"
},
{
"path": "crates/oa-core/src/view_options.rs",
"bytes": 24481,
"sha256": "421EDE0A04C593CA1DF0E6E0D06FABCFCC8113CDBCFB0ADC7F5C8EF4D1724208"
},
{
"path": "crates/oa-core/src/view_options_tests.rs",
"bytes": 21433,
"sha256": "64AAC44DE2DF42A1012239A3DD75C473DAF0C8990B3170B7BF7E78A556080208"
},
{
"path": "crates/oa-core/tests/communication_boundary_scenarios.rs",
"bytes": 8816,
"sha256": "E1100B8F363910A487D90C73ACC7DF543DC6D532158C3C63AD11CD3D40D8E12B"
},
{
"path": "crates/oa-core/tests/communication_means.rs",
"bytes": 18102,
"sha256": "A25F2749180BC6F4BEB1445BFAD3351EEDF0669131A7439B633AB804439E95A4"
},
{
"path": "crates/oa-core/tests/coverage_diagnostics.rs",
"bytes": 20527,
"sha256": "E2E9C3996DAAB47F2BD23F5D8C48B4E372AF33331152F5FC85B76F06D04FCDF4"
},
{
"path": "crates/oa-core/tests/fixtures/v2-coastal-rescue.arcadia",
"bytes": 29220,
"sha256": "F0DF129C1914BFC01D0B047B0CD4B520D8CF0C249EA9CED085DE400C0BE87FF3"
},
{
"path": "crates/oa-core/tests/fixtures/v2-coastal-rescue.json",
"bytes": 43990,
"sha256": "3ED53508EB544BBD788832F7BEBEA2EC31B3D66191F24F45098CC9750EC4C005"
},
{
"path": "crates/oa-core/tests/fixtures/v3-data-contract.arcadia",
"bytes": 2450,
"sha256": "305F19D1B79F99F8D4CF627BA2E9CA6836BB7959CC0070227815937A699EAFEA"
},
{
"path": "crates/oa-core/tests/fixtures/v3-data-contract.json",
"bytes": 8240,
"sha256": "1B41299C53204E6232397D25A58D5BCCDBBC16E770C75E8EDB55CE36CCFE58A2"
},
{
"path": "crates/oa-core/tests/fixtures/v4-reviewed-prose.arcadia",
"bytes": 1222,
"sha256": "5FA53A26A5AD010C8E59EB56D9B2B52003569E66C0377FFB58840590168AD0C1"
},
{
"path": "crates/oa-core/tests/fixtures/v4-reviewed-prose.json",
"bytes": 1734,
"sha256": "2808088AFCC165F4BD6DA5129151143411C997041C3DE61D600AB76E05D24A47"
},
{
"path": "crates/oa-core/tests/language.rs",
"bytes": 56482,
"sha256": "77F6CB2A69976BFB5F60B243B71C07FB0EDC7505002C1E9E73FCD6DDF6D9496E"
},
{
"path": "crates/oa-core/tests/port_anchors.rs",
"bytes": 26893,
"sha256": "EFBC0FA18733B1AAAC501F9785A4C392FC49C05C45A5778D26E9B5DD951CA6FA"
},
{
"path": "crates/oa-core/tests/port_migration.rs",
"bytes": 15620,
"sha256": "1801C376A5435AC7EFB2A8342348B2C7FFEF930BBA65DECA4ACB66B06E0483D7"
},
{
"path": "crates/oa-core/tests/port_source.rs",
"bytes": 4858,
"sha256": "8D2DDACA4045D330F6EDA2591561497B858AAD53769A823249D0BEA4F5916E12"
},
{
"path": "crates/oa-core/tests/prepared_commands.rs",
"bytes": 9555,
"sha256": "52CAB1C668064777412DF0A6A3C20D74D579B33794B4B00525B9F151C06EAC9B"
},
{
"path": "crates/oa-core/tests/process_diagnostics.rs",
"bytes": 1104,
"sha256": "1DBBC590239EF7542BE4A2BDC9543AAA64D108417B03DD312A0894E364745906"
},
{
"path": "crates/oa-core/tests/r3_constructor_ack.rs",
"bytes": 24163,
"sha256": "E5C44676EA0AAB97F2E091DDAC2F4BC4A4A90F5877F7BBF08B61229600944DD0"
},
{
"path": "crates/oa-core/tests/r3_migration.rs",
"bytes": 6590,
"sha256": "EBD04120F5215B3956FDC79EE78F6E38734C7D13392F1FC28AABA957912265BD"
},
{
"path": "crates/oa-core/tests/r3_semantic.rs",
"bytes": 77473,
"sha256": "49C3763EAF2131070A7C46CC23A84F917D292D41610B6404510E5607F8890C32"
},
{
"path": "crates/oa-core/tests/r3_source.rs",
"bytes": 15311,
"sha256": "F31F60659DF3C7EF601650D1CFF140589D8729F01A3F65A4A6A6C2272F15C408"
},
{
"path": "crates/oa-core/tests/r6_data.rs",
"bytes": 41437,
"sha256": "5F7FFB3BC73C3C85423623FE56F8680646DC97040327BDBEAD9D47A37E80F155"
},
{
"path": "crates/oa-core/tests/r6_migration.rs",
"bytes": 9174,
"sha256": "81519BC1512F5E069432E9A1CDAD04C10216D492DF01B4B75B02102480EBD6A1"
},
{
"path": "crates/oa-core/tests/r6_source.rs",
"bytes": 9519,
"sha256": "88A0AA087A7678E2898FF6057538072CA92072190712F05FFF08372CC56EBED6"
},
{
"path": "crates/oa-core/tests/release1_teaching.rs",
"bytes": 10010,
"sha256": "BE179290D871125DC97CFD2A3067C7AC89C8A538FDB0AFAFC5B825BCDFB2BEF2"
},
{
"path": "crates/oa-core/tests/release_capabilities.rs",
"bytes": 19422,
"sha256": "B92CBA2A562E78287DDD7DC3E94557848D17FAA0C8A102601B50286B9AF48A30"
},
{
"path": "crates/oa-core/tests/release_diagrams.rs",
"bytes": 7689,
"sha256": "CEFCC0582D91266D5DE6EE5DAEFCFE41B606AB971AB235B59034A8E1DBC24B7D"
},
{
"path": "crates/oa-core/tests/release_language_limits.rs",
"bytes": 5204,
"sha256": "8578DA4F76BBD2D0E1BFE295AB1259E2162A3FE3D0D2BA8901915D7A40942806"
},
{
"path": "crates/oa-core/tests/release_ports.rs",
"bytes": 32740,
"sha256": "506E62BEAB0C7EC0CECE83E9E0C1A810FA1F99285C9E1CAB0C77F06DE55A40E1"
},
{
"path": "crates/oa-core/tests/release_routing.rs",
"bytes": 23165,
"sha256": "AE168FAABA23769B7CA1FD65F217B9DB671726E67276EB5A04E5E72B1447A462"
},
{
"path": "crates/oa-core/tests/release_scenarios.rs",
"bytes": 41067,
"sha256": "0F5D876BADEEC653FC7B12610A77D867E9A53582E605253DEFF8D379DA21572F"
},
{
"path": "crates/oa-core/tests/release_states.rs",
"bytes": 26873,
"sha256": "B1A26ACEE95EABA7F9920F9DA738A1E33606552EB078E6D298895DAFF7E3D811"
},
{
"path": "crates/oa-core/tests/release_topology.rs",
"bytes": 17643,
"sha256": "C51DBE5AA5A038F9ED830DAA02AA67714472179EB7FD6DA5CE36DC6931C8B3A9"
},
{
"path": "crates/oa-core/tests/review_regressions.rs",
"bytes": 21823,
"sha256": "F154E5C88D35E31512E2B07733A1327EA3E2FCFD1EF8EB23066A4E9573F387C1"
},
{
"path": "crates/oa-core/tests/route_source.rs",
"bytes": 7270,
"sha256": "1A73FDBAC0D0AE0E84D8D67B6AD398E11223958F5DCA007F73757C91827DECFB"
},
{
"path": "crates/oa-core/tests/routing_geometry.rs",
"bytes": 31890,
"sha256": "2D8EEA4A2BBA535ED0EA6DAABCCDAF87B375828C404BAA10518C7BD7FD6253D3"
},
{
"path": "crates/oa-core/tests/routing_labels.rs",
"bytes": 5530,
"sha256": "DFF884D799C3C5C9257210248C55767E294E3CB317C605CBD0DDC9600C216EB6"
},
{
"path": "crates/oa-core/tests/semantic_artifact_absence_oracle.rs",
"bytes": 4640,
"sha256": "D93EAF9667F4221232168BEF0FE9F069E5BE1B1F23410BDA5DD67010B96E5C8A"
},
{
"path": "crates/oa-core/tests/semantic_attachments_gui_oracle.rs",
"bytes": 14957,
"sha256": "7F526D363BA05E7AC89FAF0FEAD0796E419C34EE204EDF44A1B46C78308B0970"
},
{
"path": "crates/oa-core/tests/source6_view_contract.rs",
"bytes": 14307,
"sha256": "FA566DE53946D40BBBD9033EDFEED177180C4DC44AFA2F2F4667FFDC622EB3BB"
},
{
"path": "crates/oa-core/tests/source7_contract.rs",
"bytes": 43271,
"sha256": "0702D3BD0173DEF4F67770458E4AF2B23F213FD5938EE70E3AE0042897EF5F1E"
},
{
"path": "crates/oa-core/tests/source_files.rs",
"bytes": 13168,
"sha256": "F0429A0B89A443458EA9AF7083C1E4E7411417A7E6B47E618BFBB9266DC164F9"
},
{
"path": "crates/oa-core/tests/source_identity_updates.rs",
"bytes": 5139,
"sha256": "F52AA38B4D317E532C641AD7463472E4B40E802E60052CF0017556FFC63DA06C"
},
{
"path": "crates/oa-core/tests/source_nested_updates.rs",
"bytes": 16892,
"sha256": "9AC3390936C0D250651EA94A1B8FAF0ADFB680F061ECECC56F2A52909B5BA855"
},
{
"path": "crates/oa-core/tests/source_project.rs",
"bytes": 45910,
"sha256": "CEF58ADB16AA4D2527DDC0628AEC3E26F0B42B4BE70014A7E360FAFDAC13A942"
},
{
"path": "crates/oa-core/tests/state_diagrams.rs",
"bytes": 23973,
"sha256": "6B02B387A3AE559CB3AD72DE24292A5ABCB76AAAC0ADBAB5D2AA5211B22CD2E5"
},
{
"path": "crates/oa-core/tests/workflows.rs",
"bytes": 33426,
"sha256": "BD843DFDDBD5724EC08F657571F887496203056FE2FA5864EF50A168A613B89B"
},
{
"path": "rust-toolchain.toml",
"bytes": 66,
"sha256": "A6A0BBD29FFAA8182DC22D1D9149709F1091E47DF40ED96EB8A78A711C66A4CE"
}
]
}
@@ -0,0 +1,16 @@
Checking windows-sys v0.61.2
Checking serde_core v1.0.229
Checking num-traits v0.2.19
Checking num-integer v0.1.47
Checking winapi-util v0.1.11
Checking once_cell v1.21.4
Checking memchr v2.8.3
Compiling oa-core v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\crates\oa-core)
Checking tempfile v3.27.0
Checking serde_json v1.0.151
Checking uuid v1.26.0
Checking thiserror v2.0.20
Checking serde v1.0.229
Checking same-file v1.0.6
Checking num-bigint v0.5.1
Finished `dev` profile [unoptimized] target(s) in 11m 49s
@@ -0,0 +1,840 @@
{
"source_commit": "7d14bd0dd1cf4a2293c00c185573fd4a7fc364ae",
"command": "cargo test --locked -p oa-core --lib analysis_numeric::tests -- --test-threads=1",
"exit_code": 0,
"passed": 26,
"failed": 0,
"ignored": 0,
"filtered_out": 42,
"test_seconds": 0.29,
"build_seconds": 385,
"scope": "Exact numeric foundation unit tests only; no schema8 model/evaluator or desktop acceptance.",
"toolchain": "Rust/Cargo1.98.1 x86_64-pc-windows-gnullvm; LLVM-MinGW20260908; jobs1",
"prior_attempt": "The broader command without --lib was intentionally stopped before tests while compiling unrelated integration executables. It supplies no pass result.",
"artifacts": {
"oa-core-tests.exe": {
"bytes": 24529920,
"sha256": "F92A4C4CF01DB0AD4810527F0D8AC2212196AEB3A4907E8EEA12FD5EDEADDB0D"
},
"libunwind.dll": {
"bytes": 190464,
"sha256": "AA282FA47BB2C4E210CEA5E470CDBDCA58568F0BE0BC73E404D86AB9CD95EF6A"
},
"numeric-unit.log": {
"bytes": 2754,
"sha256": "DF7F71D037CF2B3D38234C834F9F399F3BB30CC4757F453D72833DE9A9C3C063"
}
},
"input_files": [
{
"path": "Cargo.lock",
"bytes": 119973,
"sha256": "065C23C94908FE499D32ED91D55A70946ED5B08ECFDA3B483EC1E635572257BF"
},
{
"path": "Cargo.toml",
"bytes": 400,
"sha256": "369F5B4B2B60705A94623C8C0B68F8E0B048F263280CE07AE0629AD353797D62"
},
{
"path": "crates/oa-core/Cargo.toml",
"bytes": 408,
"sha256": "6B5C062474EBB62D5082E07DABED99EE85E4A833EDF26690FB4F887028D3416F"
},
{
"path": "crates/oa-core/build.rs",
"bytes": 45,
"sha256": "AB1BE6AE1FD8BF3EBAEEF6348347A9D5E666BDE693126F199E041659E0B094A9"
},
{
"path": "crates/oa-core/examples/generate_demo.rs",
"bytes": 795,
"sha256": "E26F432C95331AECA50985DBB3AD2C558AFFD43023B585811F0F7BFAA59F6661"
},
{
"path": "crates/oa-core/examples/generate_release1.rs",
"bytes": 1798,
"sha256": "1D5F11165D74893E4BC18DBF3A2F2111F4026BD89FD06EE7F0BAE69E45C4D4B5"
},
{
"path": "crates/oa-core/examples/release_dense_views.rs",
"bytes": 14664,
"sha256": "A83158488EF9B7394F136C52B1496719BE935C4E5BDFE457CAB5C9894F5F9412"
},
{
"path": "crates/oa-core/examples/routing_workshop.rs",
"bytes": 17248,
"sha256": "571307B18B6D4FA04700C5A4D4B0C2154C419E93A21292B2028E7B6F46A2FAA7"
},
{
"path": "crates/oa-core/src/analysis_numeric.rs",
"bytes": 3735,
"sha256": "6C88EEF3119162AEB6C07B9E75812C3AF9FBFE75554B0503AD00F103CB478408"
},
{
"path": "crates/oa-core/src/analysis_numeric/dimension.rs",
"bytes": 3045,
"sha256": "D4AD8E5307A1620E31791D724A0E31FDBA860D00DC83E3E1F2209D9C5E22A340"
},
{
"path": "crates/oa-core/src/analysis_numeric/exact.rs",
"bytes": 17338,
"sha256": "4EB8CB0F66B10E5DA204899AF82EE26ADFEC74BAB47B1A6C9842B5488EE04C4D"
},
{
"path": "crates/oa-core/src/analysis_numeric/fixtures/r7-literal-boundaries.json",
"bytes": 6085,
"sha256": "3B959DEC5E100A6F03817F5314BEF497347BA579EBD235476516D4BD83399181"
},
{
"path": "crates/oa-core/src/analysis_numeric/meter.rs",
"bytes": 7129,
"sha256": "1F7174BEB992935B4C399ACDEF91114AF73FAF041070FEAB968455AC827E8EE4"
},
{
"path": "crates/oa-core/src/analysis_numeric/quantity.rs",
"bytes": 12257,
"sha256": "EAD5142D6EEEA8F99399D57E02D8F7D819980923DE76387CD454694D6A747AEE"
},
{
"path": "crates/oa-core/src/analysis_numeric/tests.rs",
"bytes": 27486,
"sha256": "323CE82FC057D36F44463569FD8EA324C7D700FDDFBF32BCE15E04BD2A66970B"
},
{
"path": "crates/oa-core/src/data.rs",
"bytes": 42696,
"sha256": "7F9C7C7DD8353CB5A38597B92CDE6D3E890203D392473187EB387D41FF37414A"
},
{
"path": "crates/oa-core/src/demo.rs",
"bytes": 20657,
"sha256": "AD62123F96C20FF023CAFC1A9849B0FA06BC62EDD1FB9701799E4599B458600C"
},
{
"path": "crates/oa-core/src/language.rs",
"bytes": 99938,
"sha256": "638F08A42E9CF4996B5D07F69B4D6F517A518369F3604D25BE1006E40E080765"
},
{
"path": "crates/oa-core/src/language/semantic_source.rs",
"bytes": 7326,
"sha256": "584B5BB59D87A7B8D44D5DD2D124C3A1323B21492B138A17BC9526C0087C9E29"
},
{
"path": "crates/oa-core/src/language_nesting_tests.rs",
"bytes": 20639,
"sha256": "BD061611E8CB6F28AB203C73E544AC98BF275EB464CD314AD5FBD25F773DF33B"
},
{
"path": "crates/oa-core/src/lib.rs",
"bytes": 549,
"sha256": "EA1D9BF5ACBA8381BA204DB12FF5B5FE1F320417D17AF517E46B90FD6BCB241C"
},
{
"path": "crates/oa-core/src/migration_v2/README.md",
"bytes": 1515,
"sha256": "FB61D3FAA33D7BDFBA73415415A0882C0FBD3E0B45DF42F972A19AEC6DA927B9"
},
{
"path": "crates/oa-core/src/migration_v2/adapter.rs",
"bytes": 9490,
"sha256": "66741B4C8354A23B8A932ECC5BF1BC3BB6E74279558757B8C35BC2157A7E51B5"
},
{
"path": "crates/oa-core/src/migration_v2/mod.rs",
"bytes": 1158,
"sha256": "857D8BB13B6814DABA6780CA770C16352943C01CB130245D055F11B3B957B3E9"
},
{
"path": "crates/oa-core/src/migration_v2/model.rs",
"bytes": 52059,
"sha256": "8AC63EB59B2E57BDECFF2477EE0634096090924D3DD1851E1C454880A889DACB"
},
{
"path": "crates/oa-core/src/migration_v2/references.rs",
"bytes": 13369,
"sha256": "33A6819ABC83139FDAEBEEBD1B553238A328637106CCE7DD5A535F1791D43E20"
},
{
"path": "crates/oa-core/src/migration_v2/source.rs",
"bytes": 21946,
"sha256": "74FEE9B24E7C912182C998AB9D9AEEC125E2009C3214959229E844C67AFE2BF6"
},
{
"path": "crates/oa-core/src/migration_v2/state_validation.rs",
"bytes": 15769,
"sha256": "E29EF071BAB0A68460DDF55C46131C6F66C022D52C3C602C37C013C3649DA1B0"
},
{
"path": "crates/oa-core/src/migration_v2/transitions.rs",
"bytes": 704,
"sha256": "F74174C532BA56B57C80B719A11FB4ADEA0A78CCC2828FFF72FE2B9EDA5A10B1"
},
{
"path": "crates/oa-core/src/migration_v2/validation.rs",
"bytes": 98736,
"sha256": "6D5E4A4E2805AE5045BF789A0485E14A61C63346587BACE2B4A91173A688F780"
},
{
"path": "crates/oa-core/src/migration_v3/README.md",
"bytes": 2128,
"sha256": "3755A63F840D7F0EADF68AFA7A9D587877A9468FF982D3610539E8A0BB067A82"
},
{
"path": "crates/oa-core/src/migration_v3/mod.rs",
"bytes": 23023,
"sha256": "F4EDB6A0B50ECFE8670973776D703047B064420CB1F05913129C00FC656B3722"
},
{
"path": "crates/oa-core/src/migration_v4.rs",
"bytes": 4020,
"sha256": "50C652964F0F309E7F3F54B6EF12C40D42FCECC0490F005995C713DF1179AF70"
},
{
"path": "crates/oa-core/src/model.rs",
"bytes": 66352,
"sha256": "4585C895C64C4A183941C92CF047A9E52FF0CBE41971E16E78D229EA9DC101FB"
},
{
"path": "crates/oa-core/src/operations.rs",
"bytes": 11405,
"sha256": "A5FFBBF54CD440316A106D1188BFB699B57DAE59F466EDCB33618C333CD19F41"
},
{
"path": "crates/oa-core/src/persistence.rs",
"bytes": 31774,
"sha256": "1C01474F906A63E552C0D590B272C95821C00A100B3E79E58E67DBBEAC341A14"
},
{
"path": "crates/oa-core/src/ports.rs",
"bytes": 45413,
"sha256": "5CAC12EEA70A54EBEBBD6BE77B63CE92E3195325C4BC7D27705C518686CBAC80"
},
{
"path": "crates/oa-core/src/projection.rs",
"bytes": 21374,
"sha256": "69ED12BACE2CC5024E151752A174AB14034AC2D05EB741FCF13E9A27521C059B"
},
{
"path": "crates/oa-core/src/projection/port_layout.rs",
"bytes": 13474,
"sha256": "A08D17D10A58BB8B206DD001B19B91D5AA3A9F743B298A4DA57D503223C20079"
},
{
"path": "crates/oa-core/src/projection/state.rs",
"bytes": 5233,
"sha256": "73A607BAC713EC7FF49A509FEFB1A53C7686DCA93EE241FFC835564D743AFB28"
},
{
"path": "crates/oa-core/src/references.rs",
"bytes": 21957,
"sha256": "93553C234F32DA35F1F41A7B83AD81C471C9803EFA78CA222CF7DE3C2F86C0BB"
},
{
"path": "crates/oa-core/src/routing.rs",
"bytes": 50309,
"sha256": "C5EBB350BA76F731F5C48D968BFBBA4B6EF68EC046EB5893D116C4637EF15080"
},
{
"path": "crates/oa-core/src/semantic.rs",
"bytes": 540,
"sha256": "F9895DD5B8FB27D10D4CFBB41C3542EF2018365A589B8D5778E5674AE5A1F239"
},
{
"path": "crates/oa-core/src/semantic/catalog.rs",
"bytes": 49407,
"sha256": "1FF74FCCA18084D178681D7FF38EE1CCAC632C27C4DE0595BA1CEAB8067AB3A4"
},
{
"path": "crates/oa-core/src/semantic/historical.rs",
"bytes": 15271,
"sha256": "EAF3284BF234312698514CDB636BC8348E1B5D746CB16B18DF777E77D312ABFB"
},
{
"path": "crates/oa-core/src/semantic/linked.rs",
"bytes": 6035,
"sha256": "6F335B14E809AECFC35DEFCAF77CB821B080D1506DBC46297912B25AF98C07A2"
},
{
"path": "crates/oa-core/src/semantic/mutate.rs",
"bytes": 22683,
"sha256": "DC2B4C4A6EB156FE468EAA8C85FD37A505C0CD8EA1F226CB69CD01689AFD3D9D"
},
{
"path": "crates/oa-core/src/semantic/review.rs",
"bytes": 51116,
"sha256": "0C157D2C327E72E89CCA3B423E71EE73798105C5BB7271439C1A101642C91E3E"
},
{
"path": "crates/oa-core/src/semantic/types.rs",
"bytes": 10405,
"sha256": "6922FE35089F12FAD43AB0CE1C6886D01A3855F90B8E2543E99802AC5E6F1BBB"
},
{
"path": "crates/oa-core/src/session.rs",
"bytes": 35237,
"sha256": "1EB77B329B2986FC6F5DC0171E77FD860375CDFD8C769A1367B445D6C49EBE00"
},
{
"path": "crates/oa-core/src/source_files.rs",
"bytes": 74803,
"sha256": "669491DBDDB8E8E6556161426DFFA1A49AC1C826D31020BFF0956D81A4D84683"
},
{
"path": "crates/oa-core/src/source_project.rs",
"bytes": 61843,
"sha256": "1394370BCC5908873B830C2C11312E6E1611AF41F46C4F43CA0A6BCB713BB466"
},
{
"path": "crates/oa-core/src/source_project/source_migration.rs",
"bytes": 4700,
"sha256": "ADD463AE85C420F853D324B00A221F0F874164CC3F3ED6BBB07557A9ED7A8C95"
},
{
"path": "crates/oa-core/src/source_project/source_structure.rs",
"bytes": 20225,
"sha256": "9F1F83315856A7025FE662C0FBB13452F31613224A03663150D6225D1DDC162F"
},
{
"path": "crates/oa-core/src/state_validation.rs",
"bytes": 15665,
"sha256": "E28D7F74385E2A78763716B34491BD73A00BFECCC085970CEB130627E3FAEE18"
},
{
"path": "crates/oa-core/src/topology_context.rs",
"bytes": 13734,
"sha256": "16DE69B297DE95D93C73808621607833B713AF811DD082F0169186AE1E5C3E2D"
},
{
"path": "crates/oa-core/src/topology_context/analysis.rs",
"bytes": 20036,
"sha256": "DEAD6194F0AA60CC46045C0E5EE10DBDBB689158C38634DFBBBC45D2BA88AC65"
},
{
"path": "crates/oa-core/src/topology_context/analysis_tests.rs",
"bytes": 5258,
"sha256": "17839263410815E96FE027D4EBA3AAE0A9AB3B82B16947E2B29EBE0D09782B56"
},
{
"path": "crates/oa-core/src/transitions.rs",
"bytes": 51998,
"sha256": "FADD1B07B89CAE4117B91ABB4F1AC9EA81FF818EF20F2175E76EE8CF21FB708D"
},
{
"path": "crates/oa-core/src/transitions/communication.rs",
"bytes": 8634,
"sha256": "2B476630A88EDD9021B9156CA12A5402E01604095F0D4002AFB452671903A028"
},
{
"path": "crates/oa-core/src/validation.rs",
"bytes": 112792,
"sha256": "7C1B4AE5E4EBBA13E99AF9158CCDE138DFC5E9A7AFB580E76FBA72D0B64BA74E"
},
{
"path": "crates/oa-core/src/validation/coverage.rs",
"bytes": 6478,
"sha256": "B08E4B344ECAD055714168C83BEFF7FE29BA4EDC53EDBFFA2EAE04E5580A4D5F"
},
{
"path": "crates/oa-core/src/view_options.rs",
"bytes": 24481,
"sha256": "421EDE0A04C593CA1DF0E6E0D06FABCFCC8113CDBCFB0ADC7F5C8EF4D1724208"
},
{
"path": "crates/oa-core/src/view_options_tests.rs",
"bytes": 21433,
"sha256": "64AAC44DE2DF42A1012239A3DD75C473DAF0C8990B3170B7BF7E78A556080208"
},
{
"path": "crates/oa-core/tests/communication_boundary_scenarios.rs",
"bytes": 8816,
"sha256": "E1100B8F363910A487D90C73ACC7DF543DC6D532158C3C63AD11CD3D40D8E12B"
},
{
"path": "crates/oa-core/tests/communication_means.rs",
"bytes": 18102,
"sha256": "A25F2749180BC6F4BEB1445BFAD3351EEDF0669131A7439B633AB804439E95A4"
},
{
"path": "crates/oa-core/tests/coverage_diagnostics.rs",
"bytes": 20527,
"sha256": "E2E9C3996DAAB47F2BD23F5D8C48B4E372AF33331152F5FC85B76F06D04FCDF4"
},
{
"path": "crates/oa-core/tests/fixtures/v2-coastal-rescue.arcadia",
"bytes": 29220,
"sha256": "F0DF129C1914BFC01D0B047B0CD4B520D8CF0C249EA9CED085DE400C0BE87FF3"
},
{
"path": "crates/oa-core/tests/fixtures/v2-coastal-rescue.json",
"bytes": 43990,
"sha256": "3ED53508EB544BBD788832F7BEBEA2EC31B3D66191F24F45098CC9750EC4C005"
},
{
"path": "crates/oa-core/tests/fixtures/v3-data-contract.arcadia",
"bytes": 2450,
"sha256": "305F19D1B79F99F8D4CF627BA2E9CA6836BB7959CC0070227815937A699EAFEA"
},
{
"path": "crates/oa-core/tests/fixtures/v3-data-contract.json",
"bytes": 8240,
"sha256": "1B41299C53204E6232397D25A58D5BCCDBBC16E770C75E8EDB55CE36CCFE58A2"
},
{
"path": "crates/oa-core/tests/fixtures/v4-reviewed-prose.arcadia",
"bytes": 1222,
"sha256": "5FA53A26A5AD010C8E59EB56D9B2B52003569E66C0377FFB58840590168AD0C1"
},
{
"path": "crates/oa-core/tests/fixtures/v4-reviewed-prose.json",
"bytes": 1734,
"sha256": "2808088AFCC165F4BD6DA5129151143411C997041C3DE61D600AB76E05D24A47"
},
{
"path": "crates/oa-core/tests/language.rs",
"bytes": 56482,
"sha256": "77F6CB2A69976BFB5F60B243B71C07FB0EDC7505002C1E9E73FCD6DDF6D9496E"
},
{
"path": "crates/oa-core/tests/port_anchors.rs",
"bytes": 26893,
"sha256": "EFBC0FA18733B1AAAC501F9785A4C392FC49C05C45A5778D26E9B5DD951CA6FA"
},
{
"path": "crates/oa-core/tests/port_migration.rs",
"bytes": 15620,
"sha256": "1801C376A5435AC7EFB2A8342348B2C7FFEF930BBA65DECA4ACB66B06E0483D7"
},
{
"path": "crates/oa-core/tests/port_source.rs",
"bytes": 4858,
"sha256": "8D2DDACA4045D330F6EDA2591561497B858AAD53769A823249D0BEA4F5916E12"
},
{
"path": "crates/oa-core/tests/prepared_commands.rs",
"bytes": 9555,
"sha256": "52CAB1C668064777412DF0A6A3C20D74D579B33794B4B00525B9F151C06EAC9B"
},
{
"path": "crates/oa-core/tests/process_diagnostics.rs",
"bytes": 1104,
"sha256": "1DBBC590239EF7542BE4A2BDC9543AAA64D108417B03DD312A0894E364745906"
},
{
"path": "crates/oa-core/tests/r3_constructor_ack.rs",
"bytes": 24163,
"sha256": "E5C44676EA0AAB97F2E091DDAC2F4BC4A4A90F5877F7BBF08B61229600944DD0"
},
{
"path": "crates/oa-core/tests/r3_migration.rs",
"bytes": 6590,
"sha256": "EBD04120F5215B3956FDC79EE78F6E38734C7D13392F1FC28AABA957912265BD"
},
{
"path": "crates/oa-core/tests/r3_semantic.rs",
"bytes": 77473,
"sha256": "49C3763EAF2131070A7C46CC23A84F917D292D41610B6404510E5607F8890C32"
},
{
"path": "crates/oa-core/tests/r3_source.rs",
"bytes": 15311,
"sha256": "F31F60659DF3C7EF601650D1CFF140589D8729F01A3F65A4A6A6C2272F15C408"
},
{
"path": "crates/oa-core/tests/r6_data.rs",
"bytes": 41437,
"sha256": "5F7FFB3BC73C3C85423623FE56F8680646DC97040327BDBEAD9D47A37E80F155"
},
{
"path": "crates/oa-core/tests/r6_migration.rs",
"bytes": 9174,
"sha256": "81519BC1512F5E069432E9A1CDAD04C10216D492DF01B4B75B02102480EBD6A1"
},
{
"path": "crates/oa-core/tests/r6_source.rs",
"bytes": 9519,
"sha256": "88A0AA087A7678E2898FF6057538072CA92072190712F05FFF08372CC56EBED6"
},
{
"path": "crates/oa-core/tests/release1_teaching.rs",
"bytes": 10010,
"sha256": "BE179290D871125DC97CFD2A3067C7AC89C8A538FDB0AFAFC5B825BCDFB2BEF2"
},
{
"path": "crates/oa-core/tests/release_capabilities.rs",
"bytes": 19422,
"sha256": "B92CBA2A562E78287DDD7DC3E94557848D17FAA0C8A102601B50286B9AF48A30"
},
{
"path": "crates/oa-core/tests/release_diagrams.rs",
"bytes": 7689,
"sha256": "CEFCC0582D91266D5DE6EE5DAEFCFE41B606AB971AB235B59034A8E1DBC24B7D"
},
{
"path": "crates/oa-core/tests/release_language_limits.rs",
"bytes": 5204,
"sha256": "8578DA4F76BBD2D0E1BFE295AB1259E2162A3FE3D0D2BA8901915D7A40942806"
},
{
"path": "crates/oa-core/tests/release_ports.rs",
"bytes": 32740,
"sha256": "506E62BEAB0C7EC0CECE83E9E0C1A810FA1F99285C9E1CAB0C77F06DE55A40E1"
},
{
"path": "crates/oa-core/tests/release_routing.rs",
"bytes": 23165,
"sha256": "AE168FAABA23769B7CA1FD65F217B9DB671726E67276EB5A04E5E72B1447A462"
},
{
"path": "crates/oa-core/tests/release_scenarios.rs",
"bytes": 41067,
"sha256": "0F5D876BADEEC653FC7B12610A77D867E9A53582E605253DEFF8D379DA21572F"
},
{
"path": "crates/oa-core/tests/release_states.rs",
"bytes": 26873,
"sha256": "B1A26ACEE95EABA7F9920F9DA738A1E33606552EB078E6D298895DAFF7E3D811"
},
{
"path": "crates/oa-core/tests/release_topology.rs",
"bytes": 17643,
"sha256": "C51DBE5AA5A038F9ED830DAA02AA67714472179EB7FD6DA5CE36DC6931C8B3A9"
},
{
"path": "crates/oa-core/tests/review_regressions.rs",
"bytes": 21823,
"sha256": "F154E5C88D35E31512E2B07733A1327EA3E2FCFD1EF8EB23066A4E9573F387C1"
},
{
"path": "crates/oa-core/tests/route_source.rs",
"bytes": 7270,
"sha256": "1A73FDBAC0D0AE0E84D8D67B6AD398E11223958F5DCA007F73757C91827DECFB"
},
{
"path": "crates/oa-core/tests/routing_geometry.rs",
"bytes": 31890,
"sha256": "2D8EEA4A2BBA535ED0EA6DAABCCDAF87B375828C404BAA10518C7BD7FD6253D3"
},
{
"path": "crates/oa-core/tests/routing_labels.rs",
"bytes": 5530,
"sha256": "DFF884D799C3C5C9257210248C55767E294E3CB317C605CBD0DDC9600C216EB6"
},
{
"path": "crates/oa-core/tests/semantic_artifact_absence_oracle.rs",
"bytes": 4640,
"sha256": "D93EAF9667F4221232168BEF0FE9F069E5BE1B1F23410BDA5DD67010B96E5C8A"
},
{
"path": "crates/oa-core/tests/semantic_attachments_gui_oracle.rs",
"bytes": 14957,
"sha256": "7F526D363BA05E7AC89FAF0FEAD0796E419C34EE204EDF44A1B46C78308B0970"
},
{
"path": "crates/oa-core/tests/source6_view_contract.rs",
"bytes": 14307,
"sha256": "FA566DE53946D40BBBD9033EDFEED177180C4DC44AFA2F2F4667FFDC622EB3BB"
},
{
"path": "crates/oa-core/tests/source7_contract.rs",
"bytes": 43262,
"sha256": "CAD4F1A135DDBD947BDF3C71D1B44F1E438725095EA8C4D893464BC721C2363F"
},
{
"path": "crates/oa-core/tests/source_files.rs",
"bytes": 13168,
"sha256": "F0429A0B89A443458EA9AF7083C1E4E7411417A7E6B47E618BFBB9266DC164F9"
},
{
"path": "crates/oa-core/tests/source_identity_updates.rs",
"bytes": 5139,
"sha256": "F52AA38B4D317E532C641AD7463472E4B40E802E60052CF0017556FFC63DA06C"
},
{
"path": "crates/oa-core/tests/source_nested_updates.rs",
"bytes": 16892,
"sha256": "9AC3390936C0D250651EA94A1B8FAF0ADFB680F061ECECC56F2A52909B5BA855"
},
{
"path": "crates/oa-core/tests/source_project.rs",
"bytes": 45910,
"sha256": "CEF58ADB16AA4D2527DDC0628AEC3E26F0B42B4BE70014A7E360FAFDAC13A942"
},
{
"path": "crates/oa-core/tests/state_diagrams.rs",
"bytes": 23973,
"sha256": "6B02B387A3AE559CB3AD72DE24292A5ABCB76AAAC0ADBAB5D2AA5211B22CD2E5"
},
{
"path": "crates/oa-core/tests/workflows.rs",
"bytes": 33426,
"sha256": "BD843DFDDBD5724EC08F657571F887496203056FE2FA5864EF50A168A613B89B"
},
{
"path": "docs/language.rst",
"bytes": 130753,
"sha256": "AC6B1D982C55F246DE2F3C844B24B874599A3C20CFBD7ED22EFE4561AC622B83"
},
{
"path": "examples/coastal-rescue.arcadia",
"bytes": 33107,
"sha256": "7E183EF9FD8154D3074303F1956CD0D9871C19E3FFC630D333B0BFFBD6E30E17"
},
{
"path": "examples/coastal-rescue.json",
"bytes": 49715,
"sha256": "242F0B031229A87AF5C4EBE5D651DEFE59C191A6B7A69DC0A77F7E203411F195"
},
{
"path": "examples/legacy-v1-coastal-rescue.arcadia",
"bytes": 3449,
"sha256": "4DBBBDF8BC7E6CB8E0A81DD678069DAE585A9E5187C59BC3665E1FDAB3F1A86A"
},
{
"path": "examples/legacy-v1-coastal-rescue.oa.json",
"bytes": 5976,
"sha256": "FD0E9DEDFB7ADB922DC0575CC2B820D40344D639F1D8052BD70363AA59DE9C24"
},
{
"path": "examples/release1/README.md",
"bytes": 11137,
"sha256": "113CFB1CF38A98B51BFF60442D86A35D636A3D80EF206BECE7D53D0AAE67F2D5"
},
{
"path": "examples/release1/evidence/repair-check.md",
"bytes": 1419,
"sha256": "F4DB17BDCA4ECBB1BF0CAEF01B2DA5F1F7F798AB3152B648CE6DD8696A6A68E5"
},
{
"path": "examples/release1/expected-diagnostics.json",
"bytes": 288,
"sha256": "3700393D76B940CFC076BF52861BD7B1F43370F8EABCE3594531BD0BF1FC851E"
},
{
"path": "examples/release1/model.json",
"bytes": 134520,
"sha256": "EF0EF4006C3E09D5DB6E1B621256B3F7D6642F3A7B09CC34607CFBCCA830C727"
},
{
"path": "examples/release1/model.rs",
"bytes": 28762,
"sha256": "15F30F6BDE655808FE754665C26D560E36F2A9BAD340F346B09137D0D05045F5"
},
{
"path": "examples/release1/oa/activities/activity_oa_restore_work.arcadia",
"bytes": 1307,
"sha256": "83E73D0A013D5C3E1A4AF0D9139AB0CFA362D25A846671FC2DAC59181061A9D4"
},
{
"path": "examples/release1/oa/behavior/process_oa_assessment_procedure.arcadia",
"bytes": 697,
"sha256": "172C32C044EF4B5BC58A1F4635CC81F48DABB4014479B47DCE7B25FEDF2DBD0D"
},
{
"path": "examples/release1/oa/behavior/process_oa_field_procedure.arcadia",
"bytes": 1781,
"sha256": "D2552DC5383E2E91CB302A790364E197F663C2EDC013A2AB1978789825C9C324"
},
{
"path": "examples/release1/oa/behavior/process_oa_response.arcadia",
"bytes": 3225,
"sha256": "CF86F46FA5C5D70592EF43F1BC1CB4D968EFD8ADD384BA88036BEF6B88AA4ABC"
},
{
"path": "examples/release1/oa/behavior/scenario_oa_conversation.arcadia",
"bytes": 1795,
"sha256": "B25BE524E98A35EBAC5E4481B7609C027B32BAF9605E8097F807560F94A77CA6"
},
{
"path": "examples/release1/oa/behavior/scenario_oa_full_scenario.arcadia",
"bytes": 1261,
"sha256": "00A1EDF2C9707A0EB62AC070C2D02467874F2C9C1F273CC6DDB53D6D65999A1C"
},
{
"path": "examples/release1/oa/behavior/state_machine_oa_monitor.arcadia",
"bytes": 2022,
"sha256": "447C9AD1359F4A31ECF975AD07C0B08D8E7E03191FB0858E2FC56F377B223CD5"
},
{
"path": "examples/release1/oa/capabilities.arcadia",
"bytes": 781,
"sha256": "FC8012E5554350B12E1CB80080F8FEAB80D933C9B33C0F2A7BB500CB7D609936"
},
{
"path": "examples/release1/oa/model.arcadia",
"bytes": 661,
"sha256": "F7CF914766EA73EC53B85E32D117EA9A9DBA52D3E038246863541AE7069B1A15"
},
{
"path": "examples/release1/oa/participants.arcadia",
"bytes": 1834,
"sha256": "951764C14179F27C10E315ACF2EA37D1FFF54C77B8441E23ADDD239E8C715CE3"
},
{
"path": "examples/release1/oa/types.arcadia",
"bytes": 788,
"sha256": "9D0396FB82DD5733EDD8D1CF52F968CA84906927C704F9CF0944A7FAE215CF49"
},
{
"path": "examples/release1/project.arcadia",
"bytes": 1958,
"sha256": "B2674E6BF77CFE0F6F0BA79C81D2E96D92F39C02CA2C6266D80E91723BFB9FDA"
},
{
"path": "examples/release1/sa/behavior/process_sa_assessment_procedure.arcadia",
"bytes": 706,
"sha256": "73928CED61C06CBC08728D7956676A50FD4C05AA5B7F1FF4F29CE27A0B0F2088"
},
{
"path": "examples/release1/sa/behavior/process_sa_field_procedure.arcadia",
"bytes": 1790,
"sha256": "886D462AF83110CBFA55DD232A667EA1446761234471357CBB878794E98E8768"
},
{
"path": "examples/release1/sa/behavior/process_sa_response.arcadia",
"bytes": 3234,
"sha256": "041785FC22842423822431BD5E6E8D16094EBD0716965E7C765FB8C1D7549E8A"
},
{
"path": "examples/release1/sa/behavior/scenario_sa_conversation.arcadia",
"bytes": 2127,
"sha256": "97276F5757EE9AA12A0DBD9D3CCD58174B3091162BB8BCCE357CEC20A45F5D85"
},
{
"path": "examples/release1/sa/behavior/scenario_sa_full_scenario.arcadia",
"bytes": 1256,
"sha256": "E12FC406B99D60A2F7768AAA0B25BA560E8605F0B8E37F3A7CDD3B8443562B74"
},
{
"path": "examples/release1/sa/behavior/state_machine_sa_monitor.arcadia",
"bytes": 2022,
"sha256": "05CE29AF99424C491C47FE2450DC6F5F8CE995511C01EC7E17BB5855BE3C9736"
},
{
"path": "examples/release1/sa/capabilities.arcadia",
"bytes": 781,
"sha256": "B6912FB3ECE439912345FA71724ACD462E716A1E5A88203A1058A5C3B2C20A4F"
},
{
"path": "examples/release1/sa/functions/function_sa_audit_thresholds.arcadia",
"bytes": 297,
"sha256": "E5A04325D52183956DCA814DD90F4E56C96C6B7DB0F4F202F203ECB49C3E0F4F"
},
{
"path": "examples/release1/sa/functions/function_sa_restore_work.arcadia",
"bytes": 2576,
"sha256": "FDDEB67308051325041FEE641FE758DDF43345B7C47B66D433616AD07FDCCDD6"
},
{
"path": "examples/release1/sa/model.arcadia",
"bytes": 661,
"sha256": "D431509251ACB859FBB98733B6D18ADD952495E10A78703FBE8F31E40BC083C4"
},
{
"path": "examples/release1/sa/participants.arcadia",
"bytes": 4012,
"sha256": "780D2EB08B217B67B55FEBB3D34846F727B8EE9018DF6C5741748647E36B2FF8"
},
{
"path": "examples/release1/sa/types.arcadia",
"bytes": 1227,
"sha256": "101FC0FBD99F1101514AA7B6DA4F8E535AAE0BB94C9E2B9E7E6C151135B28660"
},
{
"path": "examples/release1/traceability.arcadia",
"bytes": 29506,
"sha256": "48A96F543456C9B5A5798FC795594E4119147FE45F11A6F109F141B7F514560F"
},
{
"path": "examples/release1/transition-before.json",
"bytes": 35801,
"sha256": "2253E6434F741687E22327C37D520ED15C043BC36025498650BC2366BB6A874D"
},
{
"path": "examples/release1/transition-request.json",
"bytes": 4276,
"sha256": "43B9A22E5F95AD3BC0EBD2478EAD0E14406854ABC989AE94DED9367B013D812A"
},
{
"path": "examples/release1/views/diagram_oa_allocation_view.arcadia",
"bytes": 2176,
"sha256": "DFC0EB61E0E2AA5BDA45524571FBFA3494F44CE19696F3E15E07438074A07BC7"
},
{
"path": "examples/release1/views/diagram_sa_allocation_view.arcadia",
"bytes": 2173,
"sha256": "B78B43A26A82EFB8FBD5374B09C329FA341F57B29F635D397B9EF8AEDE71D389"
},
{
"path": "examples/release1/views/diagram_sa_capability_view.arcadia",
"bytes": 1620,
"sha256": "75E8DC9FB30F25D6F8FD3C6B595593AB4F98958A25798F961E336E0FAC557649"
},
{
"path": "examples/release1/views/diagram_sa_service_view.arcadia",
"bytes": 2209,
"sha256": "B08C84F3EA633E7C78DFDAD3272F75415F6256CF3614B6D99BFD4F0269076C9B"
},
{
"path": "examples/release1/views/diagram_sa_state_view.arcadia",
"bytes": 1931,
"sha256": "5F42F4C605E57B817D8400ED21F2B14BFC4E1592AEBE941FF113767504C2B869"
},
{
"path": "examples/routing-workshop.arcadia",
"bytes": 66431,
"sha256": "DC6B75DA72884D399132ED9E300FA5A26B73D802BB526C9AF986AE9EEB0A646A"
},
{
"path": "examples/routing-workshop.json",
"bytes": 100281,
"sha256": "8592714F75B271133F21CE017D87896B6CF91EA0E4837F1C2ED453E21BE26B14"
},
{
"path": "examples/source-project/oa.arcadia",
"bytes": 2142,
"sha256": "5F60642F211B2D352A951092FAEEB7503D4FB7F6F7FCA99E810347231101D31C"
},
{
"path": "examples/source-project/project.arcadia",
"bytes": 606,
"sha256": "787F6B67B179FFD221F7E5FAF1A4EDD74CFD116AA1E3B5E9ED9F22FBFF612D8D"
},
{
"path": "examples/source-project/sa/assurance.arcadia",
"bytes": 682,
"sha256": "AB76044836E4DCD42A82D316846EC87DA2108939AA2037640C5BCA1D2ECDEE0F"
},
{
"path": "examples/source-project/sa/participants.arcadia",
"bytes": 1231,
"sha256": "1ABAD579812E77877B45A3D8E3C3E9DA734BE2D04A0254CC730A5257EBD2DC88"
},
{
"path": "examples/source-project/sa/reporting.arcadia",
"bytes": 2839,
"sha256": "F0B7443F728912CA163DE39B6055876B533A77FEAA3219B2D40A2DD65712E058"
},
{
"path": "examples/source-project/views.arcadia",
"bytes": 3508,
"sha256": "E29AD3F406A6A50D2F0AE2CA2A53EE3B0C18222CD4098704A46D4F48B64EA17F"
},
{
"path": "examples/text-first/README.md",
"bytes": 1367,
"sha256": "4EC8250D5ED556F1D496E8C85137E973E802ECB11DF1A36488411E8CDC5A9F9E"
},
{
"path": "examples/text-first/project.arcadia",
"bytes": 480,
"sha256": "F441535C623E0CF20E1875EB2CD33B587ED00E078C37AF39CB36D89BB5EDC19A"
},
{
"path": "rust-toolchain.toml",
"bytes": 66,
"sha256": "A6A0BBD29FFAA8182DC22D1D9149709F1091E47DF40ED96EB8A78A711C66A4CE"
}
]
}

Some files were not shown because too many files have changed in this diff Show More