Compare commits

...
96 changed files with 16975 additions and 2783 deletions
+8
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,4 @@ 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
+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 -->
+265 -20
View File
@@ -5,6 +5,7 @@ mod checkpoints;
#[cfg(test)]
mod diagram_selection_tests;
mod hosted;
mod reference_authoring;
#[cfg(test)]
mod review_navigation_tests;
#[cfg(test)]
@@ -318,6 +319,7 @@ pub struct FilePrompt {
#[derive(Debug, Clone)]
pub enum Message {
BehaviorControl(behavior::canvas_workspace::Intent),
Architecture(architecture_authoring::Event),
Home(crate::home::Event),
CheckpointTick,
@@ -371,6 +373,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,
@@ -445,10 +449,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,
@@ -547,10 +555,13 @@ impl App {
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,
@@ -854,8 +865,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 +1038,10 @@ impl App {
crate::model_menu::popup(self),
)
.into();
crate::hosted::view::overlay(self, base)
behavior::canvas_workspace::focus_root(
crate::hosted::view::overlay(self, self.reference_overlay(base)),
behavior::canvas_workspace::Scope::input_owner(self),
)
}
pub fn subscription(&self) -> Subscription<Message> {
Subscription::batch([
@@ -1031,8 +1049,10 @@ 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),
behavior::creation::subscription(
self.behavior.creation.has_pending() || self.behavior.source_active(),
)
.map(|()| Message::Escape),
iced::event::listen_with(|event, status, _| {
let iced::Event::Keyboard(keyboard::Event::KeyPressed {
key,
@@ -1077,6 +1097,108 @@ impl App {
])
}
pub fn update(&mut self, message: Message) -> Task<Message> {
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 !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
@@ -1299,6 +1421,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;
@@ -1368,6 +1491,7 @@ impl App {
}
fn handle(&mut self, message: Message) -> Task<Message> {
match message {
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 +1507,17 @@ impl App {
Message::ScenarioCanvas(event) => {
use crate::behavior::scenario::{self, CanvasEvent};
let context = event.context();
if let CanvasEvent::FormShown(_, session) = event {
if self.scenario_context().is_some_and(|current| {
current.document == context.document
&& current.scenario == context.scenario
&& current.revision == context.revision
}) && self.behavior.scenario.take_form_focus(session)
{
return iced::widget::operation::focus(scenario::NAME_INPUT);
}
return Task::none();
}
if let CanvasEvent::Viewport(_, size) = event {
if self.scenario_context().is_some_and(|current| {
current.document == context.document
@@ -1420,6 +1555,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 +1611,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()
@@ -1519,6 +1699,37 @@ impl App {
return Task::none();
}
match mutation {
CanvasEvent::First(_, tool) => {
let empty = match tool {
scenario::Tool::Lifeline => !self
.project()
.children(context.scenario)
.any(|e| e.kind() == Kind::Lifeline),
scenario::Tool::Message | scenario::Tool::Action => {
self.project()
.children(context.scenario)
.any(|e| e.kind() == Kind::Lifeline)
&& !self
.project()
.children(context.scenario)
.any(|e| e.kind() == Kind::Occurrence)
}
_ => false,
};
if !empty {
self.status="This scenario already has content. Use its current authoring tools.".into();
return Task::none();
}
let mut state = std::mem::take(&mut self.behavior.scenario);
let result = state.update(
self.project(),
scenario::Event::Start(context.scenario, tool),
);
self.behavior.scenario = state;
if let Err(error) = result {
self.status = error;
}
}
CanvasEvent::BeginOccurrence {
tool,
slot,
@@ -1526,13 +1737,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 +1974,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 +2045,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
@@ -2321,10 +2552,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 +2576,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);
}
@@ -2537,13 +2776,19 @@ impl App {
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"
+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,
+60 -15
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);
@@ -0,0 +1,300 @@
//! 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(semantic_review::Event::Row {
address,
facet: Some(facet),
event,
})) => {
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,550 @@
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(), Default::default());
}
}
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)));
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().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,890 @@
//! 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::{
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},
};
#[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)).into()
}
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(
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.layer() == Layer::Sa
&& matches!(
element.shared(),
Some(SharedBody::Occurrence {
body: OccurrenceBody::Exchange {
operation: Some(_),
message_kind: MessageKind::Call | MessageKind::Asynchronous,
..
},
..
})
)
})
.unwrap()
.id;
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()
.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(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());
}
+129 -31
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,7 +393,7 @@ mod scenario_scroll {
std::time::Instant::now(),
))
};
for area in [ScrollArea::Canvas, ScrollArea::Page] {
for area in [ScrollArea::Page] {
for returned_height in [600., 250.] {
let (project, [a, _, _], [use_id, _, _]) = navigation_fixture();
let mut app = App::with_project(project.clone());
@@ -742,6 +730,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 +883,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 +942,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()
@@ -1085,6 +1142,7 @@ struct Scene {
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>,
@@ -1139,6 +1197,7 @@ impl Scene {
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.),
@@ -1801,9 +1860,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 {
@@ -1830,7 +1892,7 @@ impl canvas::Program<Message> for Scene {
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,10 +1902,18 @@ 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,
occurrence,
parent: self.rows[&occurrence].0,
start: cursor.position()? - self.scroll_offset,
moved: false,
});
@@ -1918,6 +1988,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 +2037,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 +2149,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 +2199,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(
@@ -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 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 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,258 @@
//! 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))
) {
if !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,753 @@
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 new(app: &mut App) -> 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");
let mut mounted = Self {
renderer,
cache: Cache::new(),
operations: 0,
};
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();
self.renderer.screenshot(
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,
)
}
}
+192 -4
View File
@@ -5,6 +5,7 @@ use super::*;
use iced::widget::{Column, column, text_input};
use std::{cell::RefCell, collections::BTreeSet};
mod workspace;
pub(crate) use workspace::NAME_INPUT;
pub use workspace::{Shelf, view};
#[cfg(test)]
@@ -94,10 +95,13 @@ impl std::fmt::Display for CanvasTool {
}
#[derive(Debug, Clone)]
pub enum CanvasEvent {
First(Context, Tool),
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),
@@ -132,10 +136,13 @@ pub enum ScrollArea {
impl CanvasEvent {
pub fn context(&self) -> Context {
match self {
Self::Scrolled(context, _, _)
Self::First(context, _)
| 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 +169,7 @@ impl std::fmt::Display for Operator {
}
#[derive(Debug, Clone)]
pub enum Event {
Source(super::source_draft::Event),
Start(Id, Tool),
Cancel,
Submit,
@@ -197,6 +205,8 @@ const VALUE_PAGE_SIZE: usize = 24;
#[derive(Default)]
pub struct State {
draft: Option<Draft>,
form_session: Id,
form_focus_pending: bool,
canvas_tool: Option<(Id, CanvasTool)>,
call_choices: RefCell<Option<(CallChoiceContext, Vec<Choice>)>>,
scroll_offsets: [iced::Vector; 2],
@@ -240,6 +250,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 +289,40 @@ 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 {
self.draft.as_mut().is_some_and(|d| d.source.back())
}
pub(crate) fn enter(&mut self, document: Id, owner: Id) {
if self.presentation.identity != Some((document, owner)) {
self.presentation = workspace::Presentation {
@@ -286,9 +337,31 @@ impl State {
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
}
@@ -356,7 +429,7 @@ impl State {
owner: frame.owner,
token: Id::new_v4(),
offsets: frame.offsets,
pending: [true; 2],
pending: [false, true],
});
}
pub(super) fn scroll_restore(
@@ -415,10 +488,25 @@ impl State {
let draft = serde_json::from_value(value).map_err(|e| e.to_string())?;
Ok(Self {
draft: Some(draft),
form_session: Id::new_v4(),
form_focus_pending: true,
..Default::default()
})
}
pub(crate) fn form_session(&self) -> Option<Id> {
self.draft.as_ref().map(|_| self.form_session)
}
pub(crate) fn take_form_focus(&mut self, session: Id) -> bool {
if self.form_session() == Some(session) && self.form_focus_pending {
self.form_focus_pending = false;
true
} else {
false
}
}
pub(crate) fn scrolled(&mut self, area: ScrollArea, offset: iced::Vector) -> bool {
if !offset.x.is_finite() || !offset.y.is_finite() || offset.x < 0. || offset.y < 0. {
return false;
@@ -431,6 +519,7 @@ impl State {
self.scroll_offsets[index] = offset;
changed
}
#[cfg(test)]
fn scroll_offset(&self) -> iced::Vector {
// The former page scroll now belongs to a sibling outline shelf.
self.scroll_offsets[0]
@@ -452,8 +541,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 +665,61 @@ 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::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)?;
}
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());
}
@@ -576,7 +728,12 @@ 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_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 +1097,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 +1420,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 +1452,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()
};
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)
}
}
+133 -4
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,
@@ -80,9 +81,10 @@ pub struct State {
tool: CanvasTool,
expanded: BTreeSet<Id>,
scroll_offsets: [iced::Vector; 2],
return_to: Vec<(Id, Id, Option<oa_core::topology_context::ContextAddress>)>,
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 +92,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 +126,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 +195,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 +279,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(),
@@ -263,10 +361,12 @@ impl State {
)
}
#[cfg(test)]
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 +608,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 +731,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()
};
@@ -867,6 +985,8 @@ 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>,
@@ -1199,6 +1319,8 @@ impl<'a> Graph<'a> {
project: p,
interaction: None,
scroll_offset: iced::Vector::ZERO,
camera_pan: iced::Vector::ZERO,
pending_insertion: None,
nodes,
edges,
ordered_paths,
@@ -1298,6 +1420,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 +1636,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 +1713,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(),
@@ -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;
+145 -28
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,6 +153,7 @@ 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;
@@ -162,11 +178,21 @@ impl State {
// 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 +214,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)
{
@@ -567,9 +650,12 @@ impl Graph<'_> {
}
_ => 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 => {
@@ -722,6 +808,22 @@ 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;
}
@@ -730,7 +832,12 @@ impl Graph<'_> {
.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))
}
}
@@ -732,7 +732,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();
@@ -1364,82 +1364,6 @@ fn direct_process_palette_preserves_fit_and_manual_camera() {
}
}
#[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));
}
}
}
}
#[test]
fn direct_process_scaled_control_and_link_gestures_use_visible_positions() {
for layer in [Layer::Oa, Layer::Sa] {
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)
+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) => {
@@ -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::new(format!("reference:{generation}:{path}"))
}
pub fn scope_id(session: ModelId) -> Id {
Id::new(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 self.on_open.is_some() {
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(self.on_open.as_ref().unwrap()(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>(
session: ModelId,
generation: ModelId,
path: &str,
step: i32,
) -> Task<Message> {
iced::advanced::widget::operate(focus_operation(session, generation, path, step)).discard()
}
@@ -0,0 +1,303 @@
//! 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,
);
let delta = Vector::new(
(ancestor.offset.x + dx).clamp(0., ancestor.capacity.x) - ancestor.offset.x,
(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,
+6 -5
View File
@@ -445,9 +445,9 @@ 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.",
"Open Tools and choose Work or Exchange, then a highlighted position. 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.",
"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.",
"Use Chain involvement from the Process/Functional chain to the capability it supports. Read Model checks for unfinished or disconnected paths.",
@@ -490,7 +490,7 @@ 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.",
"Use Add first lifeline on an empty scenario, or + Lifeline. 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 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.",
@@ -543,7 +543,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 +902,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",
+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),
+75 -3
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(_, _)
@@ -747,13 +817,15 @@ 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)))
}
other => other,
})
});
crate::forms::reference_chooser::trigger::modal(view)
}
}
@@ -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,6 +922,8 @@ 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)))
@@ -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> {
+11 -6
View File
@@ -45,16 +45,21 @@ cross-representation evaluators guide implementation boundaries.
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
2. Implement the user-required common canvas interactions for Process and Scenario
in issue2 after the bounded shared-viewport/domain-constraint design decision.
Treat this as direct-manipulation acceptance, not a tool-shelf restyle.
3. Exchange exact verified commits with the separate host, preserving its workspace
ownership. Direct authenticated publication works. CLI01b and exact evidence2ec
are imported; next contextual-source and B4 handoffs retain their owners.
4. 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
5. Exact numeric/unit/work-meter foundation26/26 unit tests pass at7d14bd0; complete
its strict gate while actual schema8 core work proceeds in an isolated worktree.
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.
6. Update current ledgers and durable decisions when evidence changes the plan.
Reassess unresolved assumptions before expanding downstream implementation.
## Coordination and evidence rules
+42 -18
View File
@@ -1,6 +1,6 @@
# Progress Ledger — first release
Updated 2026-09-11, coordination through Gitea #190. This ledger describes the
Updated 2026-09-11, coordination through Gitea #245 and issue2. 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
@@ -20,8 +20,10 @@ 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.
are outside those artifacts and await a coherent build. Current committed source
adds exact numeric foundation7d14bd0, imported CLIbc311bc and evidence/native
dc0f2e1. Numeric26/26 unit tests and strict core all-target Clippy pass. Shared
instruction ancestrycab01c3 is merged asc7ed894, retaining the exact uploaded goal.
The authoritative goal was copied byte-identically from attachment
`a49d1780-25d5-4778-ad68-6140c60e4207/goal-objective.md`; SHA256
`DD58F75C93D55FE16ED141A23D834898FB6EBC625AE7E7AE379DA552A8312435`.
@@ -74,14 +76,26 @@ 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.
Preserved OA snapshots end in `oa-reopened/`; separate SA Action Save/Undo/Redo
also passes complete oracle/history comparisons, ending in `sa-action-redo/`,
63objects. The GUI is saved at SA A caller, no active form. SA State/Reuse/Fragment
and final reopen remain. No 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.
source is now frozen, with exact row/facet/group/attachment guards. Independent
review exposed and closed three focus defects at source level, including the
legitimate Review rationale input. Fourteen new App/mounted tests await execution;
the earlier isolated13 is separately scoped. [Review](release-reference-chooser-production-review-20260911.md).
User [issue2](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/2)
requires Process and Scenario to behave as actual canvases like architecture/state
diagrams. Current scrollable diagrams/tool shelves do not close that requirement.
behavior_audit owns a bounded shared-canvas design; root re-observed the saved
native scenario and its restricted viewport, without new model input.
The [baseline contract](release-analysis-baseline-contract-20260911.md) supplies a
complete17-UUID proposed model,11 exact fixtures, explicit ModelBaseline/Case,
@@ -98,27 +112,37 @@ 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,
not a passing production type-checker. Exact arithmetic, unit/dimension operations
and terminal work meter are declared/committed at7d14bd0; their26/26 unit gate has
[exact source/artifact evidence](release-analysis-numeric-gate-20260911.md).
requirements_delta now owns actual schema8 definitions, expressions, commands,
references, migration and static admission in the isolated
`.local/analysis-core-worktree-20260911/`, based at7d14bd0. It remains uncompiled.
semantics_critic reviews the bounded decoder strategy separately; existing default
recursion rejection stays until a full allocation/lifecycle gate is justified.
Root owns compilation. Numeric/runtime and static-admission ceilings remain
explicitly provisional pending measured gates. Approximate engineering calculations,
full evaluator/model/source/REST/GUI parity and actual library maintenance are open.
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
prerequisite creation and source CLI. CLI01b76a1 is imported asbc311bc; exact
corrected evidence2ec72d4 is imported atdc0f2e1, preserving root's separate native
report. External675 staged-source and CLI12/183-assertion results remain externally
attributed. Its runnable Studio/server preview release1 and standalone CLI preview
release2 are published; see their release notes for exact source/runtime limits.
Strict language/API HTML regeneration and the72-fixture source7 schema gate pass
locally after that evidence import. 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.
Updated AGENTS d93cddac adds coordinated runnable milestone prereleases. Gitea
Updated AGENTS4b8465c8 adds concise shared maintenance to milestone releases. 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.
Direct authenticated push/readback passed on this host, most recently publishing
dc0f2e1. The subsequent owner's instruction commitcab01c3 has been fetched and
merged locally; no force push or additional credentials are required.
Historical verified bundle exchange remains reproducible: c6d296a bundle on
`codex/behavior-transfer-20260910`,334830 decoded bytes, SHA256
`526A80E2D32ED0E57A7B2372B5F02D4652E8751DFEBA81953C4F045225D6AACB`,
@@ -130,8 +154,8 @@ Older bundles/logs retain historical scope in Git and the archived ledger.
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.
2. Publish coherent checkpoints directly; integrate9e8r6t's next staged-source
work, then ytr83r B4 only after its coherent handoff. CLI/evidence import is done.
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,
+11 -3
View File
@@ -10,8 +10,12 @@ prototype results retain their separate scope.
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.
reopen. Separate native SA Action/history also passes. Exact numeric foundation
is declared at7d14bd0 and passes26/26 unit tests plus strict core all-target Clippy.
Working workspace/production chooser changes remain outside compiled artifacts;
independent chooser source review closed three focus findings,14 new host tests
are prepared. Schema8 core implementation is isolated and uncompiled. External
results retain their host/source attribution.
## Goal and success criteria
@@ -49,10 +53,14 @@ cannot establish completion.
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;
Updated AGENTS4b8465c8 requires coordinated runnable milestone prereleases and
concise current shared maintenance with unique historical evidence preserved;
user Gitea#178 authorizes programmatic existing-MCP-credential reuse for relevant
Git/REST publication without printing/committing secrets. Restart and usage reset
remain unauthorized.
- Gitea issue2 explicitly requires actual Process/Scenario canvases with the same
interaction approach as architecture/state diagrams. Existing pointer edits
inside scrollable projections do not by themselves satisfy that requirement.
## Verified facts
@@ -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"
}
]
}
@@ -0,0 +1,58 @@
# Contextual-source verification evidence, 2026-09-11
Codex-9e8r6t /root/release_audit. Root authorized this preservation-only directory while running separate native checks. No new tests, compilation, GUI actions, source edits, Git staging or publication were performed to create this archive.
## Scope and byte preservation
The 37 original records (201,835 bytes) were copied byte-for-byte from the two private evidence directories. `inventory.json` records their original repository-relative paths, sizes and SHA-256 values, plus the generated README and source readback record. It excludes itself to avoid a recursive hash. The directory contains 40 files total. No executable, DLL, rlib, model output, credential or dependency tree is included.
`desktop/` preserves the original relative layout of `.local/contextual-source-20260911/`. `oracle-preparation/` preserves concise records from `.local/staged-source-oracle-20260911/`. Embedded paths, timestamps, line endings, compiler diagnostics and manifest wording are deliberately unmodified. They describe the original host and run; they do not assert that private executables or dependency paths exist in another checkout.
Root should preserve these bytes through Git using the same narrow attribute convention as neighboring experiments:
```gitattributes
harness/experiments/contextual-source-20260911/** -text whitespace=cr-at-eol
```
This worker did not modify the root attributes or index. The existing global text normalization would otherwise change some CRLF records. Verify every inventory hash again after staging or a checkout with that rule.
## Results and distinct source snapshots
| Evidence | Meaning |
| --- | --- |
| `desktop/focused1*` | Missing private topology Draft Clone; compiler error, Cargo101, no tests. The already-failed compiler was stopped; raw diagnostic retains its process exit code. |
| `desktop/focused2*` | Extra guide `explain` argument; compiler error, Cargo101, no tests, same stopped-failed-compiler policy. |
| `desktop/focused3*` | Test called private App method; compiler error, Cargo101, no tests. Corrected to the actual Message route. |
| `desktop/focused4*` | Successful contextual-source22/22, 2.04s; frozen input manifest and terminal status. |
| `desktop/tests-verified/` | Root's full675/675, 448.33s test duration, plus exact executable identity `6E677F58...BE38CC0`. This is the earlier snapshot. |
| `desktop/clippy1*` | Completed strict-lint failure on three constant-event `then` expressions; later corrected to `then_some`. |
| `desktop/focused5*` | Final contextual-source23/23, 0.98s. The seven-file delta adds the captured document/exact owner recovery regression and narrow guide/lint corrections. Both dated input manifests are retained (about44KB each) because their metadata and hashes bind different executions. |
| `desktop/guide-final*` | Final guide3/3, 0.20s. |
| `desktop/clippy2*` | Final all-target strict Clippy with warnings denied, PASS, 4m29s. |
| `desktop/gui-final*` | Matching final Windows GUI build PASS, 6m20s. |
| `desktop/fmt-final-status.json` | Workspace formatting PASS. The original runner produced no `fmt-final.log` file because there was no output; no substitute log was fabricated. |
| `desktop/tests-final/manifest.json` | Final test executable `405577A3...626F38`, inventory676; affected23 and guide3 were executed. |
| `desktop/gui-verified/manifest.json` | Final GUI `52DE82E4...246ACCE`, with runtime identity. Native acceptance is a separate root task. |
**There is no full final676 pass in this evidence.** The earlier full675 result must not be relabeled as execution of the later binary. Input manifests identify source-file hashes and contemporaneous Git context, not a claim that uncommitted desktop changes were contained in the listed commit or that every external build input was archived.
The original `run-final-gates.ps1` and `stage-final.ps1` are preserved as raw records. They document serialized commands, failure stops, unchanged-input checks and refusal to overwrite frozen artifacts. They assume repository cwd, the task-local Windows toolchain and private output paths; they were not run from this archive. Any future rerun needs an explicitly coordinated compiler slot and fresh output paths, and produces new evidence rather than replacing these records.
## Independent oracle preparation
`oracle-preparation/manifest.json` binds the independently authored evaluator, fixed two-file baseline, designated core rlib and executable. `build-initial.log` is an actual zero-byte successful diagnostics log, accompanied by its exit/time JSON. Self-check and baseline logs/status passed. The small CLI baseline check records admission with expected warnings, without source writes. These records establish evaluator preparation and falsifier execution, not acceptance of GUI-produced staged-source objects. The already-owned oracle source, fixture and report remain at `harness/staged-source-authoring-contract.rs`, `harness/fixtures/staged-source-authoring/` and `harness/release-staged-source-authoring-oracle-20260911.md`; they are not duplicated here.
`source-freeze-readback.json` is a new preservation-time check: all13 desktop source hashes matched the final focused5 snapshot. The implementation and independent-review reports remain outside this directory and were not edited by this task.
To verify this archive without running any application, from this directory:
```powershell
$inventory = Get-Content -LiteralPath inventory.json -Raw | ConvertFrom-Json
foreach ($entry in $inventory.files) {
$file = Get-Item -LiteralPath $entry.path
if ($file.Length -ne $entry.bytes -or
(Get-FileHash -LiteralPath $file.FullName -Algorithm SHA256).Hash -ne $entry.sha256) {
throw "Evidence mismatch: $($entry.path)"
}
}
```
@@ -0,0 +1,5 @@
{
"started": "2026-09-11T12:02:24.4104250Z",
"finished": "2026-09-11T12:06:15.0919494Z",
"exitCode": 101
}
@@ -0,0 +1,57 @@
Checking oa-core v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\crates\oa-core)
Compiling oa-desktop v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\crates\oa-desktop)
Compiling oa-server v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\crates\oa-server)
error: unnecessary closure used with `bool::then`
--> crates\oa-desktop\src\behavior\scenario\workspace.rs:199:65
|
199 | button("Add first lifeline").on_press_maybe(clean.then(|| {
| _________________________________________________________________^
200 | | Message::ScenarioCanvas(CanvasEvent::First(context, Tool::Lifeline))
201 | | }))
| |______________________^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#unnecessary_lazy_evaluations
= note: `-D clippy::unnecessary-lazy-evaluations` implied by `-D warnings`
= help: to override `-D warnings` add `#[allow(clippy::unnecessary_lazy_evaluations)]`
help: use `then_some` instead
|
199 ~ button("Add first lifeline").on_press_maybe(clean.then_some({
200 + Message::ScenarioCanvas(CanvasEvent::First(context, Tool::Lifeline))
201 ~ }))
|
error: unnecessary closure used with `bool::then`
--> crates\oa-desktop\src\behavior\scenario\workspace.rs:209:64
|
209 | button("Add first message").on_press_maybe(clean.then(|| {
| ________________________________________________________________^
210 | | Message::ScenarioCanvas(CanvasEvent::First(context, Tool::Message))
211 | | })),
| |______________________^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#unnecessary_lazy_evaluations
help: use `then_some` instead
|
209 ~ button("Add first message").on_press_maybe(clean.then_some({
210 + Message::ScenarioCanvas(CanvasEvent::First(context, Tool::Message))
211 ~ })),
|
error: unnecessary closure used with `bool::then`
--> crates\oa-desktop\src\behavior\scenario\workspace.rs:212:63
|
212 | button("Add first action").on_press_maybe(clean.then(|| {
| _______________________________________________________________^
213 | | Message::ScenarioCanvas(CanvasEvent::First(context, Tool::Action))
214 | | }))
| |______________________^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#unnecessary_lazy_evaluations
help: use `then_some` instead
|
212 ~ button("Add first action").on_press_maybe(clean.then_some({
213 + Message::ScenarioCanvas(CanvasEvent::First(context, Tool::Action))
214 ~ }))
|
error: could not compile `oa-desktop` (bin "arcadia-studio" test) due to 3 previous errors
@@ -0,0 +1,15 @@
{
"started": "2026-09-11T12:25:20.3859405Z",
"finished": "2026-09-11T12:29:50.4525461Z",
"exitCode": 0,
"args": [
"clippy",
"--locked",
"-p",
"oa-desktop",
"--all-targets",
"--",
"-D",
"warnings"
]
}
@@ -0,0 +1,2 @@
Checking oa-desktop v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\crates\oa-desktop)
Finished `dev` profile [unoptimized] target(s) in 4m 29s
@@ -0,0 +1,11 @@
{
"started": "2026-09-11T12:36:12.8744967Z",
"finished": "2026-09-11T12:36:41.0827202Z",
"exitCode": 0,
"args": [
"fmt",
"--all",
"--",
"--check"
]
}
@@ -0,0 +1,5 @@
{
"finished": "2026-09-11T11:27:02.1571291Z",
"started": "2026-09-11T11:23:44.5463460Z",
"exitCode": 101
}
@@ -0,0 +1,33 @@
Compiling oa-desktop v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\crates\oa-desktop)
error[E0308]: mismatched types
--> crates\oa-desktop\src\behavior\topology.rs:197:31
|
197 | self.draft = Some(next);
| ---- ^^^^ expected `Draft`, found `&Draft`
| |
| arguments to this enum variant are incorrect
|
note: `behavior::topology::Draft` does not implement `Clone`, so `&behavior::topology::Draft` was cloned instead
--> crates\oa-desktop\src\behavior\topology.rs:187:28
|
187 | let mut next = d.clone();
| ^
help: the type constructed contains `&behavior::topology::Draft` due to the type of the argument passed
--> crates\oa-desktop\src\behavior\topology.rs:197:26
|
197 | self.draft = Some(next);
| ^^^^^----^
| |
| this argument influences the type of `Some`
note: tuple variant defined here
--> /rustc/48a229ceaefd4985c50990b14116b6d856af0985/library\core\src\option.rs:606:4
help: consider annotating `behavior::topology::Draft` with `#[derive(Clone)]`
|
102 + #[derive(Clone)]
103 | struct Draft {
|
error: could not compile `oa-desktop` (bin "arcadia-studio" test) due to 1 previous error
Caused by:
process didn't exit successfully: `C:\Users\User\Documents\Git\ARCADIA-Studio\.local\toolchain\rustup\toolchains\stable-x86_64-pc-windows-gnullvm\bin\rustc.exe --crate-name arcadia_studio --edition=2024 crates\oa-desktop\src\main.rs --error-format=json --json=diagnostic-rendered-ansi,artifacts,future-incompat --emit=dep-info,link -C embed-bitcode=no --test --check-cfg cfg(docsrs,test) --check-cfg "cfg(feature, values())" -C metadata=e92d7ab29d07ce2a -C extra-filename=-58e30cee1fd51a8b --out-dir C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\deps -C linker=clang.exe -C incremental=C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\incremental -C strip=debuginfo -L dependency=C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\deps --extern axum=C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\deps\libaxum-ccc7aea466db9258.rlib --extern iced=C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\deps\libiced-ee73abd722c72024.rlib --extern iced_runtime=C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\deps\libiced_runtime-97354eb606c5cbe8.rlib --extern oa_core=C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\deps\liboa_core-6c8da57367eec642.rlib --extern oa_server=C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\deps\liboa_server-69305e51c40cee45.rlib --extern reqwest=C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\deps\libreqwest-31ac987472fa8038.rlib --extern rfd=C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\deps\librfd-17cc7ba2db223d7b.rlib --extern serde=C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\deps\libserde-60a32311146a42e4.rlib --extern serde_json=C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\deps\libserde_json-da2c4f333fc47818.rlib --extern tempfile=C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\deps\libtempfile-86aac28d6be16d2f.rlib --extern tokio=C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\deps\libtokio-5e0ca96735e3073a.rlib -L native=C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\build\ring-5f8d2d20440f09b4\out -L native=C:\Users\User\Documents\Git\ARCADIA-Studio\.local\toolchain\cargo\registry\src\index.crates.io-1949cf8c6b5b557f\windows_x86_64_gnullvm-0.52.6\lib -C link-arg=C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\build\oa-desktop-664d65ac5124e646\out\normal-user-manifest.o` (exit code: 0xffffffff)
@@ -0,0 +1,5 @@
{
"finished": "2026-09-11T11:39:43.0079347Z",
"started": "2026-09-11T11:37:10.5178164Z",
"exitCode": 101
}
@@ -0,0 +1,26 @@
Compiling oa-desktop v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\crates\oa-desktop)
error[E0061]: this function takes 2 arguments but 3 arguments were supplied
--> crates\oa-desktop\src\guide.rs:899:25
|
899 | ... explain(
| ^^^^^^^
...
902 | ... "Behavior → New keeps the exact name, owner ...d raw text, for correction or cancellation.",
| --------------------------------------------...-------------------------------------------- unexpected argument #3 of type `&'static str`
|
note: function defined here
--> crates\oa-desktop\src\guide.rs:150:10
|
150 | const fn explain(title: &'static str, body: &'static str) -> Section {
| ^^^^^^^
help: remove the extra argument
|
901 - "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.",
902 - "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.",
901 + "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.",
|
error: could not compile `oa-desktop` (bin "arcadia-studio" test) due to 1 previous error
Caused by:
process didn't exit successfully: `C:\Users\User\Documents\Git\ARCADIA-Studio\.local\toolchain\rustup\toolchains\stable-x86_64-pc-windows-gnullvm\bin\rustc.exe --crate-name arcadia_studio --edition=2024 crates\oa-desktop\src\main.rs --error-format=json --json=diagnostic-rendered-ansi,artifacts,future-incompat --emit=dep-info,link -C embed-bitcode=no --test --check-cfg cfg(docsrs,test) --check-cfg "cfg(feature, values())" -C metadata=e92d7ab29d07ce2a -C extra-filename=-58e30cee1fd51a8b --out-dir C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\deps -C linker=clang.exe -C incremental=C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\incremental -C strip=debuginfo -L dependency=C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\deps --extern axum=C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\deps\libaxum-ccc7aea466db9258.rlib --extern iced=C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\deps\libiced-ee73abd722c72024.rlib --extern iced_runtime=C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\deps\libiced_runtime-97354eb606c5cbe8.rlib --extern oa_core=C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\deps\liboa_core-6c8da57367eec642.rlib --extern oa_server=C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\deps\liboa_server-69305e51c40cee45.rlib --extern reqwest=C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\deps\libreqwest-31ac987472fa8038.rlib --extern rfd=C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\deps\librfd-17cc7ba2db223d7b.rlib --extern serde=C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\deps\libserde-60a32311146a42e4.rlib --extern serde_json=C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\deps\libserde_json-da2c4f333fc47818.rlib --extern tempfile=C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\deps\libtempfile-86aac28d6be16d2f.rlib --extern tokio=C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\deps\libtokio-5e0ca96735e3073a.rlib -L native=C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\build\ring-5f8d2d20440f09b4\out -L native=C:\Users\User\Documents\Git\ARCADIA-Studio\.local\toolchain\cargo\registry\src\index.crates.io-1949cf8c6b5b557f\windows_x86_64_gnullvm-0.52.6\lib -C link-arg=C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\build\oa-desktop-664d65ac5124e646\out\normal-user-manifest.o` (exit code: 0xffffffff)
@@ -0,0 +1,5 @@
{
"started": "2026-09-11T11:48:07.8443256Z",
"exitCode": 101,
"finished": "2026-09-11T11:49:20.9876952Z"
}
@@ -0,0 +1,16 @@
Compiling oa-desktop v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\crates\oa-desktop)
error[E0624]: method `execute` is private
--> crates\oa-desktop\src\behavior\contextual_source_tests.rs:1112:17
|
1112 | assert!(app.execute(Command::Rename(owner, "Accepted renamed scenario".into())));
| ^^^^^^^ private method
|
::: crates\oa-desktop\src\app.rs:851:5
|
851 | fn execute(&mut self, command: Command) -> bool {
| ----------------------------------------------- private method defined here
error: could not compile `oa-desktop` (bin "arcadia-studio" test) due to 1 previous error
Caused by:
process didn't exit successfully: `C:\Users\User\Documents\Git\ARCADIA-Studio\.local\toolchain\rustup\toolchains\stable-x86_64-pc-windows-gnullvm\bin\rustc.exe --crate-name arcadia_studio --edition=2024 crates\oa-desktop\src\main.rs --error-format=json --json=diagnostic-rendered-ansi,artifacts,future-incompat --emit=dep-info,link -C embed-bitcode=no --test --check-cfg cfg(docsrs,test) --check-cfg "cfg(feature, values())" -C metadata=e92d7ab29d07ce2a -C extra-filename=-58e30cee1fd51a8b --out-dir C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\deps -C linker=clang.exe -C incremental=C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\incremental -C strip=debuginfo -L dependency=C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\deps --extern axum=C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\deps\libaxum-ccc7aea466db9258.rlib --extern iced=C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\deps\libiced-ee73abd722c72024.rlib --extern iced_runtime=C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\deps\libiced_runtime-97354eb606c5cbe8.rlib --extern oa_core=C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\deps\liboa_core-6c8da57367eec642.rlib --extern oa_server=C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\deps\liboa_server-69305e51c40cee45.rlib --extern reqwest=C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\deps\libreqwest-31ac987472fa8038.rlib --extern rfd=C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\deps\librfd-17cc7ba2db223d7b.rlib --extern serde=C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\deps\libserde-60a32311146a42e4.rlib --extern serde_json=C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\deps\libserde_json-da2c4f333fc47818.rlib --extern tempfile=C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\deps\libtempfile-86aac28d6be16d2f.rlib --extern tokio=C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\deps\libtokio-5e0ca96735e3073a.rlib -L native=C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\build\ring-5f8d2d20440f09b4\out -L native=C:\Users\User\Documents\Git\ARCADIA-Studio\.local\toolchain\cargo\registry\src\index.crates.io-1949cf8c6b5b557f\windows_x86_64_gnullvm-0.52.6\lib -C link-arg=C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\build\oa-desktop-664d65ac5124e646\out\normal-user-manifest.o` (exit code: 0xffffffff)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,5 @@
{
"exitCode": 0,
"finished": "2026-09-11T11:59:25.8739914Z",
"started": "2026-09-11T11:49:26.5831603Z"
}
@@ -0,0 +1,30 @@
Compiling oa-desktop v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\crates\oa-desktop)
Finished `test` profile [unoptimized] target(s) in 9m 56s
Running unittests src\main.rs (.local\target-gnullvm\debug\deps\arcadia_studio-58e30cee1fd51a8b.exe)
running 22 tests
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_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_old_empty_action_cannot_open_a_previous_owner_or_layer ... 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 result: ok. 22 passed; 0 failed; 0 ignored; 0 measured; 653 filtered out; finished in 2.04s
@@ -0,0 +1,37 @@
[
{
"path": "crates/oa-desktop\\src\\behavior\\contextual_source_tests.rs",
"before": "42E3E985EBA327B518D731AE0714AD5F9B48AF991E9FD62B2B0666E139E02487",
"after": "5F05318A6533503B9D6FEBF1EC4DD7256F192394BC1B952A7904C8175BD8DDAE"
},
{
"path": "crates/oa-desktop\\src\\behavior\\creation.rs",
"before": "AB851BCBC3EB070D5911F878EE43FE0530FCB67269103B4B56EF73E17F5BC104",
"after": "CA7275B5C0118CBB78F5D16254E50FADDC26DBB53816D01950064C852E296B2E"
},
{
"path": "crates/oa-desktop\\src\\behavior\\scenario.rs",
"before": "5ED3FD63433EF51A6D6315FC4F8251995BBCAA29A0A938FAC0921A4845FAAB2A",
"after": "D12A4B1CAA61C08F69C4D2A64BD15568C4B7158DEA91AAC5A52A58A35ADB863C"
},
{
"path": "crates/oa-desktop\\src\\behavior\\scenario\\workspace.rs",
"before": "6100D1A6CE09CD3C2836EB1559DADF382437DB34DB2BDA76CEB4E479CB15DA41",
"after": "69C7AD32E4BDC39F42D2E6C7764CFE1FA7867CD7DBCF9B904DEC805E6C912666"
},
{
"path": "crates/oa-desktop\\src\\behavior\\source_draft.rs",
"before": "7447D38E178C3B199901E519ABDCA6868CFD52510F7B67A5EA8B849072CDAE2D",
"after": "68F396C420FFF132FF0EAB82CB2C6251477412F6F9F90AD2E76C51A259F8A69A"
},
{
"path": "crates/oa-desktop\\src\\behavior\\topology.rs",
"before": "40139E3714E702E1F331F98EE2579E167D3CA61330864772EC5E40614F118A5A",
"after": "D06B2F0FC39B4814E94200297E148F1D4D97E04CB167766030AC9DF18689E3B0"
},
{
"path": "crates/oa-desktop\\src\\guide.rs",
"before": "C6425F2FBC2D7D04015E15D78F602CA838019B7C15352EF5A380F78A30800C68",
"after": "47EEB3CC0A48D5712720096015A08387CD354FB91559B0BD6FA106B5CAC6292C"
}
]
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,16 @@
{
"started": "2026-09-11T12:20:22.2107248Z",
"finished": "2026-09-11T12:25:14.9560639Z",
"exitCode": 0,
"args": [
"test",
"--locked",
"-p",
"oa-desktop",
"--bin",
"arcadia-studio",
"contextual_source",
"--",
"--test-threads=1"
]
}
@@ -0,0 +1,31 @@
Compiling oa-desktop v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\crates\oa-desktop)
Finished `test` profile [unoptimized] target(s) in 4m 48s
Running unittests src\main.rs (.local\target-gnullvm\debug\deps\arcadia_studio-58e30cee1fd51a8b.exe)
running 23 tests
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_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_old_empty_action_cannot_open_a_previous_owner_or_layer ... 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 result: ok. 23 passed; 0 failed; 0 ignored; 0 measured; 653 filtered out; finished in 0.98s
@@ -0,0 +1,13 @@
{
"started": "2026-09-11T12:29:50.4564653Z",
"finished": "2026-09-11T12:36:12.8525359Z",
"exitCode": 0,
"args": [
"build",
"--locked",
"-p",
"oa-desktop",
"--bin",
"arcadia-studio"
]
}
@@ -0,0 +1,2 @@
Compiling oa-desktop v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\crates\oa-desktop)
Finished `dev` profile [unoptimized] target(s) in 6m 20s
@@ -0,0 +1,18 @@
{
"created": "2026-09-11T12:37:04.6742598Z",
"scope": "Final matching Windows GUI build, strictClippy and formatting passed; native acceptance pending root",
"sourceInputs": "../focused5-inputs.json",
"sourceDeltaFromFull675": "../focused5-delta.json",
"artifacts": [
{
"file": "arcadia-studio.exe",
"sha256": "52DE82E434B636E18DB02D381842AA6DB84B721CFFD5502063F784F37246ACCE",
"bytes": 112219136
},
{
"file": "libunwind.dll",
"sha256": "F45604A12E8BCD839FD86F667C1BE1CC7C4F9DA2B3C69FF381AC708F0E1AE7D4",
"bytes": 190976
}
]
}
@@ -0,0 +1,16 @@
{
"started": "2026-09-11T12:25:15.0326187Z",
"finished": "2026-09-11T12:25:20.3798172Z",
"exitCode": 0,
"args": [
"test",
"--locked",
"-p",
"oa-desktop",
"--bin",
"arcadia-studio",
"guide",
"--",
"--test-threads=1"
]
}
@@ -0,0 +1,10 @@
Finished `test` profile [unoptimized] target(s) in 4.40s
Running unittests src\main.rs (.local\target-gnullvm\debug\deps\arcadia_studio-58e30cee1fd51a8b.exe)
running 3 tests
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 result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 673 filtered out; finished in 0.20s
@@ -0,0 +1,22 @@
$ErrorActionPreference = 'Stop'
. ./.local/toolchain/env.ps1
$env:CARGO_BUILD_JOBS = '1'
$evidenceRoot = Join-Path (Get-Location) '.local/contextual-source-20260911'
$gates = @(
@{name='focused5'; args=@('test','--locked','-p','oa-desktop','--bin','arcadia-studio','contextual_source','--','--test-threads=1')},
@{name='guide-final'; args=@('test','--locked','-p','oa-desktop','--bin','arcadia-studio','guide','--','--test-threads=1')},
@{name='clippy2'; args=@('clippy','--locked','-p','oa-desktop','--all-targets','--','-D','warnings')},
@{name='gui-final'; args=@('build','--locked','-p','oa-desktop','--bin','arcadia-studio')},
@{name='fmt-final'; args=@('fmt','--all','--','--check')}
)
foreach ($gate in $gates) {
$logPath = Join-Path $evidenceRoot ($gate.name + '.log')
$statusPath = Join-Path $evidenceRoot ($gate.name + '-status.json')
if ((Test-Path -LiteralPath $logPath) -or (Test-Path -LiteralPath $statusPath)) { throw "Refusing to overwrite $($gate.name)" }
$started = [DateTime]::UtcNow.ToString('o')
$gateArgs = $gate.args
& cargo @gateArgs 2>&1 | Tee-Object -FilePath $logPath
$gateExit = $LASTEXITCODE
[ordered]@{started=$started;finished=[DateTime]::UtcNow.ToString('o');exitCode=$gateExit;args=$gateArgs} | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath $statusPath
if ($gateExit -ne 0) { exit $gateExit }
}
@@ -0,0 +1,32 @@
$ErrorActionPreference = 'Stop'
$evidenceRoot = Join-Path (Get-Location) '.local/contextual-source-20260911'
foreach ($gate in 'focused5','guide-final','clippy2','gui-final','fmt-final') {
$status = Get-Content -LiteralPath (Join-Path $evidenceRoot ($gate + '-status.json')) -Raw | ConvertFrom-Json
if ($status.exitCode -ne 0) { throw "$gate has not passed" }
}
$inputs = Get-Content -LiteralPath (Join-Path $evidenceRoot 'focused5-inputs.json') -Raw | ConvertFrom-Json
foreach ($inputFile in $inputs.inputs) {
if ((Get-FileHash -LiteralPath $inputFile.path -Algorithm SHA256).Hash -ne $inputFile.sha256) { throw "Input changed: $($inputFile.path)" }
}
$testLog = Get-Content -LiteralPath (Join-Path $evidenceRoot 'focused5.log') -Raw
if ($testLog -notmatch 'Running unittests[^\r\n]+\(([^\r\n]+\.exe)\)') { throw 'No executed test binary in focused5 log' }
$testPath = $Matches[1]
$runtimePath = Join-Path $evidenceRoot 'tests-verified/libunwind.dll'
if ((Get-FileHash -LiteralPath $runtimePath -Algorithm SHA256).Hash -ne 'F45604A12E8BCD839FD86F667C1BE1CC7C4F9DA2B3C69FF381AC708F0E1AE7D4') { throw 'Runtime hash mismatch' }
$stages = @(
@{directory='tests-final';source=$testPath;name='arcadia-studio-tests.exe';scope='Final affected contextual-source and guide tests; broader full675 belongs to earlier tests-verified snapshot'},
@{directory='gui-verified';source='.local/target-gnullvm/debug/arcadia-studio.exe';name='arcadia-studio.exe';scope='Final matching Windows GUI build, strictClippy and formatting passed; native acceptance pending root'}
)
foreach ($stage in $stages) {
$destination = Join-Path $evidenceRoot $stage.directory
if (Test-Path -LiteralPath $destination) { throw "Refusing to overwrite $destination" }
New-Item -ItemType Directory -Path $destination | Out-Null
Copy-Item -LiteralPath $stage.source -Destination (Join-Path $destination $stage.name)
Copy-Item -LiteralPath $runtimePath -Destination (Join-Path $destination 'libunwind.dll')
$artifacts = foreach ($filename in $stage.name,'libunwind.dll') {
$item = Get-Item -LiteralPath (Join-Path $destination $filename)
[ordered]@{file=$filename;sha256=(Get-FileHash -LiteralPath $item.FullName -Algorithm SHA256).Hash;bytes=$item.Length}
}
[ordered]@{created=[DateTime]::UtcNow.ToString('o');scope=$stage.scope;sourceInputs='../focused5-inputs.json';sourceDeltaFromFull675='../focused5-delta.json';artifacts=@($artifacts)} | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath (Join-Path $destination 'manifest.json')
Get-Content -LiteralPath (Join-Path $destination 'manifest.json') -Raw
}
@@ -0,0 +1,18 @@
{
"created": "2026-09-11T12:37:00.9856183Z",
"scope": "Final affected contextual-source and guide tests; broader full675 belongs to earlier tests-verified snapshot",
"sourceInputs": "../focused5-inputs.json",
"sourceDeltaFromFull675": "../focused5-delta.json",
"artifacts": [
{
"file": "arcadia-studio-tests.exe",
"sha256": "405577A35CE643AC022885AF58706D6F8B321224AE6D68E2555FBFAF3D626F38",
"bytes": 107101696
},
{
"file": "libunwind.dll",
"sha256": "F45604A12E8BCD839FD86F667C1BE1CC7C4F9DA2B3C69FF381AC708F0E1AE7D4",
"bytes": 190976
}
]
}
@@ -0,0 +1,680 @@
running 675 tests
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_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_scroll_and_real_resize_invalidate_only_pointer_context ... 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_scroll_resize_and_passive_geometry_keep_drafts_and_current_origin ... 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::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::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_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_old_empty_action_cannot_open_a_previous_owner_or_layer ... 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_scroll_resize_and_bounds_changes_cancel_captured_slots ... ok
test behavior::direct_canvas_tests::direct_scrolled_canvas_maps_raw_pointer_events_to_the_visible_slot ... 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_observed_scroll_current_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::scenario_workspace_allocates_remaining_height_and_pins_actions_at_minimum_and_wide_sizes ... ok
test behavior::scenario::workspace::tests::scenario_workspace_caller_restores_actual_mounted_canvas_and_shelf_with_camera ... ok
test behavior::scenario::workspace::tests::scenario_workspace_canvas_and_form_scroll_survive_disclosures_focus_width_and_resize ... 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_reobserves_same_sized_owner_and_document_changes ... 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_scroll_after_preselection ... ok
test behavior::scenario::workspace::tests::scenario_workspace_scaled_and_scrolled_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::workspace::tests::scenario_workspace_whole_app_canvas_tree_survives_pending_shelves_and_focus ... 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_pending_form_diff_retains_the_canvas_scroll_state ... ok
test behavior::topology::direct_tests::direct_process_production_click_click_drag_insert_and_scroll_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_fit_canvas_selection_ignores_retained_ordered_page_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::direct_process_pending_editor_uses_available_height_and_keeps_actions_visible ... ok
test behavior::topology::view::layout_tests::process_fit_layout_disclosures_and_pending_editor_keep_real_manual_canvas_scroll ... ok
test behavior::topology::view::layout_tests::process_fit_layout_measures_remaining_canvas_with_details_forms_and_window_resize ... ok
test behavior::topology::view::layout_tests::process_fit_whole_app_focus_and_disclosures_retain_canvas_tree_with_pending_raw_form ... 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::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. 675 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 448.33s
@@ -0,0 +1,8 @@
{
"command": "arcadia-studio-tests.exe --test-threads=1",
"started": "2026-09-11T12:01:47.3639508+00:00",
"ended": "2026-09-11T12:09:15.9218331+00:00",
"seconds": 448.5578823,
"exit_code": 0,
"executable_sha256": "6E677F58458B6122EA1886AAC98882640CD9CF260651FE3ECE62BDDA4BE38CC0"
}
@@ -0,0 +1,17 @@
{
"artifacts": [
{
"sha256": "6E677F58458B6122EA1886AAC98882640CD9CF260651FE3ECE62BDDA4BE38CC0",
"file": "arcadia-studio-tests.exe",
"bytes": 107061248
},
{
"sha256": "F45604A12E8BCD839FD86F667C1BE1CC7C4F9DA2B3C69FF381AC708F0E1AE7D4",
"file": "libunwind.dll",
"bytes": 190976
}
],
"created": "2026-09-11T12:00:28.0285570Z",
"testResult": "focused4:22 passed,0 failed,653 filtered; test inventory675",
"sourceInputs": "../focused4-inputs.json"
}
@@ -0,0 +1,244 @@
{
"format_version": 1,
"created_utc": "2026-09-11T13:04:09.8807042Z",
"scope": "37 byte-exact raw evidence records and 2 generated explanatory/readback files; inventory excludes itself",
"raw_file_count": 37,
"raw_bytes": 201835,
"directory_file_count": 40,
"files": [
{
"path": "desktop/clippy1-status.json",
"source": ".local/contextual-source-20260911/clippy1-status.json",
"bytes": 118,
"sha256": "2D85A6E841E743B564B7AAFFA756F372739567F7728EE616B7122076218B77D6"
},
{
"path": "desktop/clippy1.log",
"source": ".local/contextual-source-20260911/clippy1.log",
"bytes": 3126,
"sha256": "75BB52CB0CAD69FF257F6C9F05D0514644975AA333BA2645BBEE41925FFFD432"
},
{
"path": "desktop/clippy2-status.json",
"source": ".local/contextual-source-20260911/clippy2-status.json",
"bytes": 257,
"sha256": "F876F56FA20383C206DBCA137E1A8999963C0A61A4B0547B22CEA67032E3827C"
},
{
"path": "desktop/clippy2.log",
"source": ".local/contextual-source-20260911/clippy2.log",
"bytes": 157,
"sha256": "DACDAADE0BFCF36EAC3550425B8D28AE0F266FE7AD57538F5B4D9C41FF8EEA87"
},
{
"path": "desktop/fmt-final-status.json",
"source": ".local/contextual-source-20260911/fmt-final-status.json",
"bytes": 187,
"sha256": "4FB53DC797C682977A180042219AA065B91C955081D0C1FA342D39CCFDE4FCA8"
},
{
"path": "desktop/focused1-status.json",
"source": ".local/contextual-source-20260911/focused1-status.json",
"bytes": 118,
"sha256": "114DBEAF0BF83EB2FDE30229D1CBEAAD9A877F804258719EACE9F71A5CDD9D5A"
},
{
"path": "desktop/focused1.log",
"source": ".local/contextual-source-20260911/focused1.log",
"bytes": 4059,
"sha256": "47D3071A768200CBF07972E602F64C586C428D444C49FD8C3757E05F33DADF2E"
},
{
"path": "desktop/focused2-status.json",
"source": ".local/contextual-source-20260911/focused2-status.json",
"bytes": 118,
"sha256": "751B73AABFB359352E70923DA243B33B2EF6EF11FBA7B3219C53372895776C40"
},
{
"path": "desktop/focused2.log",
"source": ".local/contextual-source-20260911/focused2.log",
"bytes": 4738,
"sha256": "5511947F161FBBA804DAF9F94A376A35D0FF7D0A137A98909C9872C25CC2BD7D"
},
{
"path": "desktop/focused3-status.json",
"source": ".local/contextual-source-20260911/focused3-status.json",
"bytes": 118,
"sha256": "71EAD5DBCA70CF34202BA02952A7291E4C0A9C65B14642FF8461377650251744"
},
{
"path": "desktop/focused3.log",
"source": ".local/contextual-source-20260911/focused3.log",
"bytes": 3303,
"sha256": "C7CF8FDBF4A5D2FAEA42053826D69791CE9235F9E6EE4350E74003DEB43F15A1"
},
{
"path": "desktop/focused4-inputs.json",
"source": ".local/contextual-source-20260911/focused4-inputs.json",
"bytes": 44197,
"sha256": "396255153B439B8427874E288E6C10D880A83CBDEDA345082B3F7D9A9722D629"
},
{
"path": "desktop/focused4-status.json",
"source": ".local/contextual-source-20260911/focused4-status.json",
"bytes": 116,
"sha256": "D1BBB26A1735523C2816003C98D6DB73841667886369B00DDB2639FAD5557FC4"
},
{
"path": "desktop/focused4.log",
"source": ".local/contextual-source-20260911/focused4.log",
"bytes": 3211,
"sha256": "3A286BC01FC19B9B48EF371E21659DFC215B7D7883D5FFD10DBC3406AAB4037A"
},
{
"path": "desktop/focused5-delta.json",
"source": ".local/contextual-source-20260911/focused5-delta.json",
"bytes": 1681,
"sha256": "14162287460DF568F2CB513146651691862DE4C16F57D165A9F047A9CA49C792"
},
{
"path": "desktop/focused5-inputs.json",
"source": ".local/contextual-source-20260911/focused5-inputs.json",
"bytes": 44179,
"sha256": "C0FB04418539D42186144BEC263F4A6D3B98BC51661B8460113E51292460A05B"
},
{
"path": "desktop/focused5-status.json",
"source": ".local/contextual-source-20260911/focused5-status.json",
"bytes": 293,
"sha256": "405169976AC379546E129E6D1FCA072B8F3660FE124B59CAE78584D61C3EC81A"
},
{
"path": "desktop/focused5.log",
"source": ".local/contextual-source-20260911/focused5.log",
"bytes": 3341,
"sha256": "80121F2815E2446891B6845140B5FCD106D78A37F543465177F5BDA4B8C8AE93"
},
{
"path": "desktop/gui-final-status.json",
"source": ".local/contextual-source-20260911/gui-final-status.json",
"bytes": 232,
"sha256": "976D790EAA2DD98CD86D56F1E8FDC571BD18DB6E1C06962E9BB4EA2F311BD935"
},
{
"path": "desktop/gui-final.log",
"source": ".local/contextual-source-20260911/gui-final.log",
"bytes": 157,
"sha256": "1D82FDE70BB41C4E2AF0DA35F970CCCBB91FC30247D28BC8D4AA6896EBE30DA5"
},
{
"path": "desktop/gui-verified/manifest.json",
"source": ".local/contextual-source-20260911/gui-verified/manifest.json",
"bytes": 609,
"sha256": "8B6F8938B66F5BB2E1BE805CB840F57765264B9C29B41845D94407037A561108"
},
{
"path": "desktop/guide-final-status.json",
"source": ".local/contextual-source-20260911/guide-final-status.json",
"bytes": 281,
"sha256": "A2F07DB7BD9FD5FCC28DE506A20EACCFFC9BE157CFB83258198443B19F21193C"
},
{
"path": "desktop/guide-final.log",
"source": ".local/contextual-source-20260911/guide-final.log",
"bytes": 560,
"sha256": "8A84A57A967097E16E320ED78818305487B5CED25159CD6DB2C54345C6D1788C"
},
{
"path": "desktop/run-final-gates.ps1",
"source": ".local/contextual-source-20260911/run-final-gates.ps1",
"bytes": 1389,
"sha256": "885C165B9937B6BDF8043AC945FF60C3CF31EAB7DB8323A0A16BD262F920124F"
},
{
"path": "desktop/stage-final.ps1",
"source": ".local/contextual-source-20260911/stage-final.ps1",
"bytes": 2638,
"sha256": "41D292C7066A265BB220BF3D67919BD531651931EEAC0301D7655A11DB668044"
},
{
"path": "desktop/tests-final/manifest.json",
"source": ".local/contextual-source-20260911/tests-final/manifest.json",
"bytes": 623,
"sha256": "0F143F45B4415C174E3D4879FB85A09851B10B2FFDC6B3F4435452C2A840EF84"
},
{
"path": "desktop/tests-verified/full-desktop.log",
"source": ".local/contextual-source-20260911/tests-verified/full-desktop.log",
"bytes": 76384,
"sha256": "DAD6B821863262F6420DF6D60D2225C529C79310E24F75D5DDA2C7892BF15D57"
},
{
"path": "desktop/tests-verified/full-result.json",
"source": ".local/contextual-source-20260911/tests-verified/full-result.json",
"bytes": 302,
"sha256": "737BC7053D7B63EC660C930EC578E6E21E3905BEE6D85A3BE9FDC9C49D97D561"
},
{
"path": "desktop/tests-verified/manifest.json",
"source": ".local/contextual-source-20260911/tests-verified/manifest.json",
"bytes": 524,
"sha256": "17AEA1E6AF2C86A77BBAE0AB41932CAE8CB82E8D197644B3C0BF2CF2A196B4C4"
},
{
"path": "oracle-preparation/baseline-check.log",
"source": ".local/staged-source-oracle-20260911/baseline-check.log",
"bytes": 82,
"sha256": "2D82DF14FB345414E164B57177D5A21C426F65C18EB5EF9E010984A92AD2E731"
},
{
"path": "oracle-preparation/baseline-cli-check-result.json",
"source": ".local/staged-source-oracle-20260911/baseline-cli-check-result.json",
"bytes": 169,
"sha256": "C9376E2458743B162E69EA67A16992D6F7D0D813636E82D8E616D0EB504A25E3"
},
{
"path": "oracle-preparation/baseline-cli-check.json",
"source": ".local/staged-source-oracle-20260911/baseline-cli-check.json",
"bytes": 1902,
"sha256": "66004F0718E5071F5F08C7DE11A4E8C503DF72EF018C799610111780D36A228F"
},
{
"path": "oracle-preparation/build-initial-result.json",
"source": ".local/staged-source-oracle-20260911/build-initial-result.json",
"bytes": 94,
"sha256": "14BF8DD87ADE8DFE2D230F350ADBC2BA9285B1488FB654AC4EB8C62C0C34F7C8"
},
{
"path": "oracle-preparation/build-initial.log",
"source": ".local/staged-source-oracle-20260911/build-initial.log",
"bytes": 0,
"sha256": "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855"
},
{
"path": "oracle-preparation/checks-initial-result.json",
"source": ".local/staged-source-oracle-20260911/checks-initial-result.json",
"bytes": 100,
"sha256": "28F505AF226299E2FF2660DF2BF73E84161D22AEA41229C6997F5B416738607B"
},
{
"path": "oracle-preparation/manifest.json",
"source": ".local/staged-source-oracle-20260911/manifest.json",
"bytes": 1709,
"sha256": "C76C7C6FF4EC1901662A0FCFB949761F6F112029D901D3BBE70E63068E83564A"
},
{
"path": "oracle-preparation/self-check-initial.log",
"source": ".local/staged-source-oracle-20260911/self-check-initial.log",
"bytes": 763,
"sha256": "6114C5BF9B75A27B4333E3DE38B3CAB08B976B2F01F3158AD8A7127FF7EE0ABB"
},
{
"path": "README.md",
"source": null,
"bytes": 5833,
"sha256": "AA559AFB8C75B6905E31F95D191BAE8688496FD1C0911330B9E2BCD043156CF6"
},
{
"path": "source-freeze-readback.json",
"source": null,
"bytes": 3766,
"sha256": "0CFA05BB91E52E6F0D4D9E11B5DBF84E0DD90DAA570D4422F3E4CCCDEA3C10E1"
}
]
}
@@ -0,0 +1 @@
PASS Baseline: complete Project + 2 exact source/disk files, original role UUIDs
@@ -0,0 +1,5 @@
{
"Command": "arcadia check harness/fixtures/staged-source-authoring/base/project.arcadia --json",
"Exit": 0,
"RecordedUtc": "2026-09-11T11:54:47.3267470Z"
}
@@ -0,0 +1,65 @@
{
"format_version": 1,
"entry": "harness/fixtures/staged-source-authoring/base/project.arcadia",
"operation": "check",
"status": "checked",
"exit_code": 0,
"written": false,
"diagnostics": [
{
"severity": "warning",
"code": "missing_system",
"message": "Identify the SA system of interest.",
"coordinate_space": "input",
"location": {
"path": "project.arcadia",
"start": 11,
"end": 121,
"line": 2,
"column": 1
},
"related": [],
"object_id": "d1510000-0000-4000-8000-000000000001",
"field": "elements"
},
{
"severity": "warning",
"code": "empty_sequence",
"message": "Add ordered occurrences.",
"coordinate_space": "input",
"location": {
"path": "project.arcadia",
"start": 375,
"end": 377,
"line": 8,
"column": 15
},
"related": [],
"object_id": "d1510000-0000-4000-8000-000000000002",
"field": "steps"
},
{
"severity": "warning",
"code": "empty_sequence",
"message": "Add ordered occurrences.",
"coordinate_space": "input",
"location": {
"path": "project.arcadia",
"start": 581,
"end": 583,
"line": 13,
"column": 15
},
"related": [],
"object_id": "d1510000-0000-4000-8000-000000000003",
"field": "steps"
}
],
"changes": [],
"identities_to_stabilize": 0,
"migration": null,
"notes": [
"Diagnostic object IDs are omitted for declarations without authored IDs; lint's temporary identities are not the candidate's identities.",
"Disk writes use the core journal/conflict checks. This is not cross-process atomic visibility or a power-loss durability guarantee; pending saves are never implicitly recovered. A failed write may require explicit journal recovery."
]
}
@@ -0,0 +1,5 @@
{
"Exit": 0,
"Seconds": 7.5659589,
"FinishedUtc": "2026-09-11T12:01:48.0539296Z"
}
@@ -0,0 +1,5 @@
{
"SelfCheckExit": 0,
"BaselineExit": 0,
"FinishedUtc": "2026-09-11T12:02:24.4408700Z"
}
@@ -0,0 +1,50 @@
{
"RecordedUtc": "2026-09-11T12:03:07.0603355Z",
"Build": {
"Exit": 0,
"Seconds": 7.5659589,
"FinishedUtc": "2026-09-11T12:01:48.0539296Z"
},
"Checks": {
"SelfCheckExit": 0,
"BaselineExit": 0,
"FinishedUtc": "2026-09-11T12:02:24.44087Z"
},
"Artifacts": [
{
"Path": "harness/staged-source-authoring-contract.rs",
"Bytes": 22693,
"SHA256": "9EB9DD3868B6CB772F907D16AE02995AD10A16F85C087012C24948E6E6FE775D"
},
{
"Path": "harness/fixtures/staged-source-authoring/base/project.arcadia",
"Bytes": 593,
"SHA256": "AA844950EC4FB86F262A257E1FC370A26557EA7978D8A4CAB78265037660234C"
},
{
"Path": "harness/fixtures/staged-source-authoring/base/notes.arcadia",
"Bytes": 63,
"SHA256": "AD9DE02963C20D1F43856455A797DEB05CA674659E45F2C4B96B52783024F8BD"
},
{
"Path": ".local/target-gnullvm/debug/deps/liboa_core-3e81c616fea1d901.rlib",
"Bytes": 99971382,
"SHA256": "5018A1FD7286ED9DFEDCBBF5B68B399AD5D143F2A9AAAED3078CFC934C9589C4"
},
{
"Path": ".local/staged-source-oracle-20260911/staged-source-authoring-contract.exe",
"Bytes": 20864512,
"SHA256": "967D9BA3C6FB3A7EFB410833903EC94556CA2D48A67D5072C8BB160F40A00F9C"
},
{
"Path": ".local/staged-source-oracle-20260911/self-check-initial.log",
"Bytes": 763,
"SHA256": "6114C5BF9B75A27B4333E3DE38B3CAB08B976B2F01F3158AD8A7127FF7EE0ABB"
},
{
"Path": ".local/staged-source-oracle-20260911/baseline-check.log",
"Bytes": 82,
"SHA256": "2D82DF14FB345414E164B57177D5A21C426F65C18EB5EF9E010984A92AD2E731"
}
]
}
@@ -0,0 +1,5 @@
PASS Lifeline: complete model/source fixed point, false-carrier metadata ignored, repeated-command refusal, one Undo/Redo, comment drift and explicit retained-file falsifiers; 3 reachable files
PASS Chain: complete model/source fixed point, false-carrier metadata ignored, repeated-command refusal, one Undo/Redo, comment drift and explicit retained-file falsifiers; 4 reachable files
PASS Machine: complete model/source fixed point, false-carrier metadata ignored, repeated-command refusal, one Undo/Redo, comment drift and explicit retained-file falsifiers; 6 reachable files
PASS wrong source binding, machine owner and view geometry rejected; no native acceptance claimed
PASS earlier-stage role identities remain pinned independently of a later carrier
@@ -0,0 +1,84 @@
{
"checked_utc": "2026-09-11T13:02:51.2773935Z",
"scope": "Read-only comparison of 13 desktop inputs with original focused5-inputs.json; no new build/test/native execution",
"files": [
{
"path": "crates/oa-desktop/src/app.rs",
"expected": "B10BFA57F331DA3928D5FE27D2AEBDB13F6054C6E3D48C4CFD24FF81726094AD",
"actual": "B10BFA57F331DA3928D5FE27D2AEBDB13F6054C6E3D48C4CFD24FF81726094AD",
"unchanged": true
},
{
"path": "crates/oa-desktop/src/app/checkpoints.rs",
"expected": "55E5BABDCABEF767AF3BC8BE9CC3153B3F5CC0695701B890820EC0225643852E",
"actual": "55E5BABDCABEF767AF3BC8BE9CC3153B3F5CC0695701B890820EC0225643852E",
"unchanged": true
},
{
"path": "crates/oa-desktop/src/behavior.rs",
"expected": "C9E0AEA6A24CC982D0D82EE2B9B66F250CDFAC037B36BF6B1B9D7DD38246CD3C",
"actual": "C9E0AEA6A24CC982D0D82EE2B9B66F250CDFAC037B36BF6B1B9D7DD38246CD3C",
"unchanged": true
},
{
"path": "crates/oa-desktop/src/behavior/creation.rs",
"expected": "CA7275B5C0118CBB78F5D16254E50FADDC26DBB53816D01950064C852E296B2E",
"actual": "CA7275B5C0118CBB78F5D16254E50FADDC26DBB53816D01950064C852E296B2E",
"unchanged": true
},
{
"path": "crates/oa-desktop/src/behavior/scenario.rs",
"expected": "D12A4B1CAA61C08F69C4D2A64BD15568C4B7158DEA91AAC5A52A58A35ADB863C",
"actual": "D12A4B1CAA61C08F69C4D2A64BD15568C4B7158DEA91AAC5A52A58A35ADB863C",
"unchanged": true
},
{
"path": "crates/oa-desktop/src/behavior/scenario/workspace.rs",
"expected": "69C7AD32E4BDC39F42D2E6C7764CFE1FA7867CD7DBCF9B904DEC805E6C912666",
"actual": "69C7AD32E4BDC39F42D2E6C7764CFE1FA7867CD7DBCF9B904DEC805E6C912666",
"unchanged": true
},
{
"path": "crates/oa-desktop/src/behavior/topology.rs",
"expected": "D06B2F0FC39B4814E94200297E148F1D4D97E04CB167766030AC9DF18689E3B0",
"actual": "D06B2F0FC39B4814E94200297E148F1D4D97E04CB167766030AC9DF18689E3B0",
"unchanged": true
},
{
"path": "crates/oa-desktop/src/behavior/topology/contextual.rs",
"expected": "EDC84FC69D8D4A8F4CE81A4452C4E2E35CFAF98ADDEED10235F392506C74E8B0",
"actual": "EDC84FC69D8D4A8F4CE81A4452C4E2E35CFAF98ADDEED10235F392506C74E8B0",
"unchanged": true
},
{
"path": "crates/oa-desktop/src/behavior/topology/direct.rs",
"expected": "413AA2009EE4CBA0941836B43E41F16A04C8FC8568B6E92DB5BB08516E95C287",
"actual": "413AA2009EE4CBA0941836B43E41F16A04C8FC8568B6E92DB5BB08516E95C287",
"unchanged": true
},
{
"path": "crates/oa-desktop/src/behavior/topology/view.rs",
"expected": "A7D806CD621BDF857C4E4861685CD3EB556B320DE3985B9CC3D9D131DAD370DC",
"actual": "A7D806CD621BDF857C4E4861685CD3EB556B320DE3985B9CC3D9D131DAD370DC",
"unchanged": true
},
{
"path": "crates/oa-desktop/src/guide.rs",
"expected": "47EEB3CC0A48D5712720096015A08387CD354FB91559B0BD6FA106B5CAC6292C",
"actual": "47EEB3CC0A48D5712720096015A08387CD354FB91559B0BD6FA106B5CAC6292C",
"unchanged": true
},
{
"path": "crates/oa-desktop/src/behavior/source_draft.rs",
"expected": "68F396C420FFF132FF0EAB82CB2C6251477412F6F9F90AD2E76C51A259F8A69A",
"actual": "68F396C420FFF132FF0EAB82CB2C6251477412F6F9F90AD2E76C51A259F8A69A",
"unchanged": true
},
{
"path": "crates/oa-desktop/src/behavior/contextual_source_tests.rs",
"expected": "5F05318A6533503B9D6FEBF1EC4DD7256F192394BC1B952A7904C8175BD8DDAE",
"actual": "5F05318A6533503B9D6FEBF1EC4DD7256F192394BC1B952A7904C8175BD8DDAE",
"unchanged": true
}
]
}
@@ -0,0 +1,46 @@
# Staged prerequisite source creation: frozen native contract
Codex-9e8r6t /root/palette_finish,2026-09-11. Prepared before inspecting any produced staged-source output. The standalone core-only evaluator is `harness/staged-source-authoring-contract.rs`. Root subsequently granted a direct-rustc window: warnings-denied compilation, self-check and exact baseline check **passed**. This validates the evaluator/fixture, not native staged-source creation. Existing immutable CLI admission of this starting fixture also succeeded with three ordinary completeness warnings. Build/provenance is in `harness/release-staged-source-authoring-oracle-20260911.md`.
Copy the whole `base` directory into a new isolated native work directory. Do not edit these source fixtures or overwrite an earlier checkpoint. Baseline has a root project, an empty explicit OA OperationalActivity scenario, an empty SA Functional chain and an independent Unicode comment include. All three persistent IDs are explicit; there are no existing activities/functions/participants, views, relations or traces.
| File | Bytes | SHA256 before native output |
| --- | ---: | --- |
| `base/project.arcadia` |593|`AA844950EC4FB86F262A257E1FC370A26557EA7978D8A4CAB78265037660234C`|
| `base/notes.arcadia` |63|`AD9DE02963C20D1F43856455A797DEB05CA674659E45F2C4B96B52783024F8BD`|
## Exact cumulative native actions
1. OA Behavior → **Operational assessment**. The viewpoint is already **OperationalActivity**; use the semantic empty-state **Add first lifeline** action. Name the lifeline **Assessment lifeline**. Open **Create new source**, choose/retain Activity and layer-root semantic parent, and name it **Assess incident work**. Child **Use** returns to the retained parent. Verify the two distinct names, then apply the parent once. Only one root Activity plus one lifeline owned by the existing scenario, representing that Activity, should be created. Scenario steps remain empty; no occurrence, allocation, relation or view is implicit.
2. SA Behavior → **System assessment**. Use **Add first function contribution**. Name the contribution **Classify this request**. Open **Create new source**, choose/retain Function and layer-root semantic parent, name it **Classify request**, and **Use**. Apply the retained parent once. Only one root Function and one Action occurrence under the existing chain should be created; the chain steps become exactly `[NEW_CONTRIBUTION]`. Its action is the new Function, lifeline is absent, conditions are empty. An isolated first contribution has no sequence link or allocation.
3. SA Behavior → **New → State machine**. Name it **Field team operating modes**. Open **Create new owner**, explicitly select **SystemActor**, retain layer-root parent, name it **Field rescue team**, and **Use**. Check **Create the first state view**, then **Create and open** once. Only the root actor, its owned machine, and the opt-in diagram/root placement should be created. The diagram is **Field team operating modes · states**, SA StateMachine projection; default options, no routes/regions/states. Root placement is independently specified at60,60,size336×104 and passed through the shared core Layout command.
After each creation, Save and preserve a fresh whole-directory **first-commit** checkpoint. Undo **once**, Save and preserve another directory; Redo **once**, Save and preserve another. After all three, use the real file picker to reopen the final project, Save and preserve a final directory. If checking child Use before Apply or cancellation, save into a separate checkpoint and compare against the preceding accepted stage: no staged prerequisite may have been adopted.
All source/consumer names, ownership, references, defaults, membership and view geometry above are fixed expectations. The oracle borrows only unique fresh UUIDv4 roles by exact kind/layer/name (plus view projection and placement target). It never copies observed aliases, descriptions, parents, bodies, geometry, source text or diagrams wholesale. Every existing source byte and complete model field is compared after constructing the expected core-command result.
## Checker invocation and identity authority
After a root-authorized direct-rustc build using a matching completed `oa_core` rlib and `--edition2024 -D warnings`, let `$oracle` identify that immutable executable and `$base` the frozen baseline entry. Other variables below are preserved entry-file paths:
```powershell
& $oracle self-check $base
& $oracle check baseline $base $base
& $oracle check lifeline $base $firstLifeline $firstLifeline
& $oracle same $base $undoLifeline $firstLifeline
& $oracle check lifeline $base $redoLifeline $firstLifeline
& $oracle check chain $base $firstChain $firstLifeline $firstChain
& $oracle same $firstLifeline $undoChain $firstChain
& $oracle check chain $base $redoChain $firstLifeline $firstChain
& $oracle check machine $base $firstMachine $firstLifeline $firstChain $firstMachine
& $oracle same $firstChain $undoMachine $firstMachine
& $oracle check machine $base $redoMachine $firstLifeline $firstChain $firstMachine
& $oracle check machine $base $reopen $firstLifeline $firstChain $firstMachine
& $oracle same $redoMachine $reopen
```
Every stage supplies **all preceding original first-commit role carriers separately**. Those carriers are themselves checked against the cumulative contract. A later creation therefore cannot replace a prior stage's UUID and declare the replacement its own role authority; Redo and reopen cannot borrow new identities from their own output. The baseline source bytes are embedded in the checker at build time, so replacing the runtime fixture does not change its expected input.
The retained-file rule matches the corrected creation-v2 harness: normal checkpoints must have exactly reachable `.arcadia` files. `same EXPECTED ACTUAL UNDO_FROM` requires complete Project and reachable SourceBundle equality, and exact physical bytes equal to the explicitly proven first-commit disk map overlaid with EXPECTED's restored reachable files. Only those known detached files may remain. Unknown, changed or missing retained files fail. No file is taken from actual Undo output to define its own expected bytes. There is no broad orphan-file exemption.
Self-check prepares deliberately false metadata/geometry role carriers, full source fixed points, complete model/byte-drift falsifiers, exact repeated-command refusal, one core-session Undo/Redo, retained-file falsifiers and independent earlier-stage identity pinning. These **passed as evaluator checks**, not native acceptance. Predicted reachable-file counts are3 after Lifeline,4 after Chain and6 after Machine. Native focus, first-action discoverability, preserved raw parent/child drafts, selection, minimum/wide layout and real one-step history still require separate observation.
@@ -0,0 +1 @@
// Preserve this independent engineering note: secours 雨 Δ.
@@ -0,0 +1,15 @@
arcadia 7;
project contextual_source_acceptance "Contextual source acceptance" id "d1510000-0000-4000-8000-000000000001" {
include "notes.arcadia";
// The viewpoint is explicit before any lifeline exists.
oa_scenario operational_assessment "Operational assessment" id "d1510000-0000-4000-8000-000000000002" {
viewpoint OperationalActivity;
steps [];
}
// Contributions are occurrences; root Functions are separate sources.
sa_functional_chain system_assessment "System assessment" id "d1510000-0000-4000-8000-000000000003" {
steps [];
}
}
@@ -0,0 +1,45 @@
# Exact numeric foundation: local execution gate
Codex-956atf `/root`, Windows, 2026-09-11. Source
`7d14bd0dd1cf4a2293c00c185573fd4a7fc364ae` passes **26/26 numeric unit tests**,
zero failed or ignored. The test binary also contains42 unrelated unit tests,
which were filtered out. This is the numeric foundation gate, not schema8 model,
calculation evaluation, source/REST/GUI parity, or release acceptance.
Command: `cargo test --locked -p oa-core --lib analysis_numeric::tests -- --test-threads=1`.
Exit0; warning-free compilation6m25s; test execution0.29s. The task-local toolchain
selects Rust/Cargo1.98.1, Windows GNU/LLVM, LLVM-MinGW20260908 and one Cargo job.
The earlier command without `--lib` was intentionally stopped before running tests
because it compiled unrelated integration executables; that attempt supplies no
numeric test result. Its log remains under `.local/logs/analysis-numeric-first-20260911.log`.
The gate executes independent small-integer arithmetic expectations, complete R7
literal/wire packets, reduced component and wider intermediate boundaries, nominal
quantity and affine-point operations, dimension/power failures, aggregate target
accounting and terminal dynamic/numeric work limits. Source-level review is in
[the independent review](release-analysis-numeric-review-20260911.md); the exact
implementation contract and its provisional limits remain in
[the implementation handoff](release-analysis-numeric-implementation-20260911.md).
Preserved [execution log](experiments/analysis-numeric-gate-20260911/numeric-unit.log)
and [manifest](experiments/analysis-numeric-gate-20260911/manifest.json) record162
source/build/fixture inputs. Every input was compared byte-for-byte with its Git
blob at7d14bd0. The immutable local executable and runtime are retained in
`.local/analysis-numeric-gate-20260911/`; executables are not committed.
| Artifact | SHA256 |
| --- | --- |
| 24,529,920-byte test executable | `F92A4C4CF01DB0AD4810527F0D8AC2212196AEB3A4907E8EEA12FD5EDEADDB0D` |
| 190,464-byte libunwind.dll | `AA282FA47BB2C4E210CEA5E470CDBDCA58568F0BE0BC73E404D86AB9CD95EF6A` |
| Exact test log | `DF7F71D037CF2B3D38234C834F9F399F3BB30CC4757F453D72833DE9A9C3C063` |
| Manifest | `CBAF29385DE79A821EF3A76C8B64C802983B50584AA504BED373A5D9A04066A1` |
Strict core all-target Clippy also passes with exit0: `cargo clippy --locked -p
oa-core --all-targets -- -D warnings`,11m49s, unchanged core/build inputs. Its
[complete log](experiments/analysis-numeric-gate-20260911/core-clippy.log) is preserved
separately from the unit-test manifest. The isolated schema8 worktree starts
from7d14bd0 and must preserve or
rerun this gate when extracting shared pure type operations. Decoder allocation,
bounded resolved identities, actual AST traversal and full-context captures remain
production integration obligations. No approximate engineering profile or model
evaluator is supplied by these numeric unit tests.
@@ -0,0 +1,80 @@
# Shared behavior canvas phase 1 — R1/R2/R3 source correction freeze
Codex-956atf `/root/behavior_canvas_resume`, 2026-09-11. Isolated `.local/behavior-canvas-worktree-20260911`, branch `codex/behavior-canvas-20260911`, base `e511c4153fb5bb484652434340e0e4c7000c93d0`. Correction lease: [issue 2 comment 377](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/2#issuecomment-377). This answers root's full independent correction review (`root harness/release-behavior-canvas-corrections-review-20260911.md`) and [comment 366](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/2#issuecomment-366).
**Source preparation only.** All 19 owned Rust paths pass scoped standalone rustfmt formatting/parse check and `git diff --check`; only ordinary CRLF normalization notices were emitted. No Cargo/rustc, build, renderer/test/native execution, installation, Git mutation or Linux/WSL/VPN work occurred. Root owns independent review, integration and coherent Windows acceptance. Linux remains paused by the user. Prior root passing gates establish other bytes, not this candidate. Previous reports remain unchanged historical records; their superseded correctness claims must be read with the independent R1/R2/R3 review.
## R1/R2: stamped toolbar admission
Production palette, Details, Panels, Canvas, Fit and zoom actions now carry `Message::BehaviorControl(Intent)`: the current document/owner/revision/camera lifetime, stable control name and boxed existing action. App validates the live clean input owner and exact lifetime before inspecting a narrow presentation-action whitelist. Only the semantic epoch interval accumulated during that still-valid camera lifetime can be normalized to the current context. The existing exact-context process/scenario adapters then handle the action. The earlier process-only unstamped epoch exception is removed. Domain authoring gestures retain their exact full-context guards; the envelope cannot dispatch arbitrary model/source commands.
Consequently a held camera drag's own ordered Pan can precede its same-batch Fit/tool/panel action in BOTH adapters. External Fit/tool/panel/bounds/caller/chooser/dirty/busy transitions terminate the lifetime and invalidate retained toolbar packets even when semantic context has returned to the same epoch. Reapplying an unchanged Fit or tool also ends the gesture. The existing held-pan Fit `pan == ZERO` assertions are preserved.
Control no longer emits an earlier press-time ControlFocus Task. Accepted pointer or keyboard activation requests focus after the action, stamped with its resulting lifetime, matching Iced's final-view action execution. Canvas activation requests the viewport directly. F6 entry/exit, Tab/Shift+Tab traversal, Enter/Space activation and Escape exit remain the defined owner-respecting policy. Stable control identity preserves a visible keyboard target through a tool change.
## R3: actual runtime UI and guarded focus passes
The mounted helper now retains `iced_runtime::user_interface::Cache` and uses the installed `UserInterface` API for input, layout, operations, nested overlays and drawing. It drains every App message from an input batch while collecting immediately ready Task actions, builds the resulting final App view, then executes actions. Output messages become the next update cycle; pending Task streams survive until completion. Widget Chain passes run synchronously against that same UI, including root and nested overlays. Unexpected native actions panic. Production WGPU initialization still uses `expect`; there are no missing-backend skips. No private gesture/focus state is seeded.
A presentation owner marker wraps the actual App root. Both Collect and its Apply pass independently check the current clean owner and exact camera lifetime before changing focus or palette scroll. This is independent of root's later scenario parent-focus marker. The test deliberately retaining Apply across ownership changes establishes the stronger retained-operation invariant; installed native Iced executes Chain passes synchronously, so no hypothetical native interleaving is claimed.
The picker regression uses public Save(true) and cancellation completion transitions. On Windows it holds/drops the unopened external picker Task, verifies busy admission, and injects SavePath(None); every widget Task is executed. It does not test a native picker dialog. The existing Linux in-app picker path is represented by CancelFile, but was not run and Linux acceptance remains user-deferred.
## Prepared regressions, all uncompiled and unexecuted
There are now **17 production App-root mounted tests plus two shared math/input tests**. All previous direct-authoring, staged source-child, form identity, geometry, exact source/snapshot and history assertions remain.
- Existing process/scenario held-middle tests retain Fit==ZERO, now require same-batch pointer activation to focus the resulting control without F6 and the following Tab to focus ZoomOut. Retained camera and toolbar packets are asserted separately across Fit, tool, Details, Panels and resize.
- Existing caller-return tests retain camera restoration/old-delta checks and additionally reject actual pre-call Fit packets after A→B→Back.
- `production_process_retained_toolbar_intents_reject_chooser_dirty_and_picker_lifetimes` and its scenario counterpart capture actual Fit/tool/Details/Panels/Canvas packets. Real chooser open/Escape, property Field/RevertProperties, and picker cancellation cycles must change lifetime without changing semantic epoch. Replays must preserve presentation, focus, source and history; dirty/busy owners also reject packets while active.
- `production_process_keyboard_reveals_and_activates_initially_offscreen_last_tool` and its scenario counterpart explicitly require the last tool to start fully below an overflowing palette, then require keyboard reveal of the whole tool, Enter activation and changed visible focus-border pixels. Connect and Fragment activation are asserted through resulting domain presentation state.
- The existing chooser/Review test now observes the newly focused nested search input, absent from root input geometry. Both delayed Collect and retained Apply must preserve it; a retained Apply stays inert after returning to the same clean canvas with a new lifetime. The actual Review field and scroll state must survive both delayed passes. The test states that retained Chain execution is deliberate.
Root filters: `canvas_workspace::tests`, `production_process_`, `production_scenario_`, `production_empty_scenario`, `production_canvas_keyboard`, then preserved direct process/scenario/source/raw/history and root parent-focus/chooser gates. These are prepared filters, not results. Pixel assertions establish only the tested visible focus borders when executed; transformed scene clipping, refusal text, native input/accessibility and all popup geometry still require their existing acceptance gates.
## Exact scope and later union
This correction changed these ten existing Rust paths relative to comment360:
- `crates/oa-desktop/src/app.rs`
- `crates/oa-desktop/src/app/process_canvas.rs`
- `crates/oa-desktop/src/behavior/canvas_workspace.rs`
- `crates/oa-desktop/src/behavior/canvas_workspace/control.rs`
- `crates/oa-desktop/src/behavior/canvas_workspace/focus.rs`
- `crates/oa-desktop/src/behavior/canvas_workspace/tests.rs`
- `crates/oa-desktop/src/behavior/scenario/workspace.rs`
- `crates/oa-desktop/src/behavior/scenario/workspace/tests.rs`
- `crates/oa-desktop/src/behavior/topology/view.rs`
- `crates/oa-desktop/src/behavior/topology/layout_tests.rs`
The candidate's total union remains the 19 Rust paths below, plus this new versioned handoff. `app/reference_authoring.rs` still contains only the previously authorized read-only reference_input_active accessor relative to its isolated baseline; no new chooser changes were made. `behavior/form_session.rs` remains root's frozen verbatim helper with SHA256 `F5027FB3A5A5599A182E7D1A10DBA56DD66EB1F23A4BA4C4A1744C9A6A5B6E61`, used by scenario session and process invocation. No new source/core/schema/REST or persisted geometry contract was introduced. Camera/control/focus changes are presentation only; existing constrained semantic commands remain unchanged.
Root must merge its I1/I2 Name session-versus-mount identity, current FormShown payload/sensor, Back/Use/Escape rearm, parent root marker and same-axis focus adjustment, five parent-focus regressions and scenario replacement-scroll regression. Preserve root's current dynamic Id::from(String), chooser Send/'static/compiler corrections and chooser fixes. Also preserve root checkpoint 3c9e6ee's boxed workflow Event::Semantic(Box<Event>), Event::semantic constructor and reference_authoring .as_ref() pattern handling. None of those root changes were imported here; do not overwrite root App/scenario/workspace wholesale.
B4 remains ytr83r-owned. Apply pan plus scale to both its clipping rectangles and clipped child text during union; preserve its renderer/lifecycle oracles. Root owns common Wiki/ledgers, commits, releases and all execution. Full phase1 acceptance and issue2 closure are not claimed. Persistent placement/lane arrangement, exact source/reopen coverage, wider process body and endpoint manipulation, fragment wrapping/operand transfer and other phase2 domain work remain outside this bounded correction.
## Frozen source fingerprints
Ordered manifest SHA256: `9EBEC63D5D8B2595FB3867E63E7C6A26C4B04DFA1C7644B840E6FA001A2F0692`. Canonical manifest bytes are each table path, one ASCII space, its uppercase SHA256 and one LF, in this order, UTF-8. These are raw isolated working-file hashes; root integration will produce a separate union identity.
| Path | SHA256 |
| --- | --- |
| `crates/oa-desktop/src/app.rs` | `F059158240FBD41491C65D7BDDE318B0089A6DFED2688A741172535154078236` |
| `crates/oa-desktop/src/app/process_canvas.rs` | `8B80661A021E2F9F8E83BD9B0858D707F067C2E42CB180A3CDFA59A1467B3DF9` |
| `crates/oa-desktop/src/app/reference_authoring.rs` | `CFD96F4A38754C5FDA1B176B342162B7BFA7BFE0DCAE2A8309953A40395D92BF` |
| `crates/oa-desktop/src/behavior.rs` | `A35D03A1A63BB8FD6D2E5D394CC41AB93A6F306F05BDD89353C6B79503F92E64` |
| `crates/oa-desktop/src/behavior/canvas_workspace.rs` | `096D667C4C8EE3732D52668A9562C665F2D9A192ABDA4E5487FCAD7621C8DBFD` |
| `crates/oa-desktop/src/behavior/canvas_workspace/viewport.rs` | `AF1C7C1E802D1207EE6C7BD66B703811A250D7B56511512AF6C0945C776E68A7` |
| `crates/oa-desktop/src/behavior/canvas_workspace/control.rs` | `851AF1E9B155D6D0D079D9164C5616FADC33E961391A6604A43EABF5158957B3` |
| `crates/oa-desktop/src/behavior/canvas_workspace/focus.rs` | `CC58CB3838F546150D22FE4DD83A3FFD52504C279FEBAEB8D0B4A6A3B8A73950` |
| `crates/oa-desktop/src/behavior/canvas_workspace/tests.rs` | `0272F78DAA97FAB75E971F1548A3E224C2F2D0A4D9D603DB90DBEB9F6D673C1E` |
| `crates/oa-desktop/src/behavior/form_session.rs` | `F5027FB3A5A5599A182E7D1A10DBA56DD66EB1F23A4BA4C4A1744C9A6A5B6E61` |
| `crates/oa-desktop/src/behavior/scenario.rs` | `59F859B246C3186AC5B156B46AEE3A0DDC38F79C1CF332C4FB03092214A43A93` |
| `crates/oa-desktop/src/behavior/scenario/workspace.rs` | `FED77DDF304FD831CC163F6B8079BB6C17AC755BF141A5D30D5E64C04CBD9677` |
| `crates/oa-desktop/src/behavior/scenario/workspace/tests.rs` | `851772CE7C1DA0479DA7D6FB16EF17038653B43DB3B8EDF5E88605AAE4830C89` |
| `crates/oa-desktop/src/behavior/topology.rs` | `5723E51A7DA4EB224D51C1659EBBFA4DC8B11557BC8BF70F6E3E1649BAD24449` |
| `crates/oa-desktop/src/behavior/topology/view.rs` | `B2D502C11426BFFB8763515F2A56AA4C9F8E203BB36D65EB34F81C123FC406A6` |
| `crates/oa-desktop/src/behavior/topology/direct.rs` | `6102219487A0717C66190153C435B5B05226D156C59C7EAAB649249ADEF73644` |
| `crates/oa-desktop/src/behavior/topology/direct_tests.rs` | `D3CD44477BDEB31256BA52AA70383CEFC9805A8B172271D357884C95DDEF11D8` |
| `crates/oa-desktop/src/behavior/topology/layout_tests.rs` | `39D9C28201EC1B044EAE848B09AB276FF7FCF9F69FE6B5E39A6863F56A0111CE` |
| `crates/oa-desktop/src/behavior/topology/contextual.rs` | `5D0AC5B3E258FC70987A00EDE1E603681D99746596F2FCC46AD2B3667D65203A` |
@@ -0,0 +1,58 @@
# Canvas phase 1 — interrupted WIP handoff
Codex-956atf /root/behavior_audit. Implementation stopped immediately on root's user-requested reboot checkpoint, 2026-09-11. **Frozen; do not resume until root's post-reboot follow-up.**
Workspace: C:/Users/User/Documents/Git/ARCADIA-Studio/.local/behavior-canvas-worktree-20260911
Branch assigned by root: codex/behavior-canvas-20260911
Verified starting commit: c0e66f94e3412b5c3495e96f7934ae94116505c9
This is intentionally unfinished and **not compile-coherent**. No Cargo/rustc/rustfmt/native/Git mutation was run by this worker. Original checkout was not edited. Root owns WIP commit/push and reboot-safe confirmation. Coordination: issue #2 comment 261; ratified design is release-unified-behavior-canvas-design-20260911.md. User clarification #2/250 explicitly permits semantic sequence constraints.
## Exact Rust paths changed
New:
- crates/oa-desktop/src/behavior/canvas_workspace.rs
- crates/oa-desktop/src/behavior/canvas_workspace/viewport.rs
Modified:
- crates/oa-desktop/src/behavior.rs
- crates/oa-desktop/src/behavior/topology.rs
- crates/oa-desktop/src/behavior/topology/direct.rs
- crates/oa-desktop/src/behavior/scenario.rs
- crates/oa-desktop/src/behavior/scenario/workspace.rs
This new handoff is the only report written in the implementation worktree. No App, core, source grammar, REST, schema, architecture/state implementation, existing tests, ledgers or prerequisites files were edited. The earlier design report was written in the original checkout under its prior report-only lease; do not confuse that completed handoff with this interrupted implementation.
## Saved implementation so far
- New shared module defines Scope (document/owner/revision), relative Pan and cursor-anchored Zoom events, Camera change helper, a constant-width tooltip palette, and a stable header/canvas/palette host with an overlaid editor. It contains no model commands.
- New generic Iced Viewport wrapper implements focusable state, ordered press-position retention, middle/Space-drag, focused arrow camera keys, wheel zoom, focus border, input capture, child rendering clipped to viewport, and delegated widget operations/overlays. No tests or compiler validated its API usage.
- Behavior declares canvas_workspace. Scene and Graph have a default-zero camera_pan, frame translation and inverse-hit coordinate changes. Existing scroll_offset remains for transitional compatibility; current production views still set/use old scroll offsets.
- Scenario Presentation and process State have a new transient pan. Both expose camera() using the shared relative-input helper. Process enter resets pan with owner/document.
- Both CanvasEvent enums gained Camera(Context, Event), and context() covers it.
- Scenario whole-glyph reorder starts a captured Reorder for a hit occurrence with a row, preserving current-parent slot validation. The current interrupted code selects an unmoved occurrence on release; this was being reviewed against existing press-selection expectations. No process free-placement behavior was added; process semantic SetSteps remains explicitly constrained.
## Known incomplete work / blockers
1. canvas_workspace.rs declares mod tests, but **canvas_workspace/tests.rs does not exist**. Create meaningful mounted tests, not an empty placeholder. No existing test assertions were weakened or changed.
2. New Camera variants have **no App handlers**; exhaustive matches may fail. Add minimal guarded adapters in App ScenarioCanvas and app/process_canvas.rs, plus Focus/Traverse Tasks. Relative camera events need document/owner/revision validation and correct same-batch ordering; root modal/pending guards must remain intact.
3. Production process/scenario view functions have **not** been migrated to Fill viewport, the new palette or the stable overlay host. They still use model-sized canvases/Scrollables and split forms. Thus the current source does not deliver phase 1.
4. Pan fields are not yet passed from presentation state into Scene/Graph, and old Fit/manual zoom paths are not reconciled with camera pan. Need explicit Fit reset/recenter and no automatic refit after editor/selection changes.
5. The new Viewport's CursorMoved branch stores the event position, but its subsequent local pointer choice can still prefer Iced's final batch cursor. Correct this before testing; actual event positions must win for ordered movement.
6. Starting camera input should invalidate an in-progress semantic gesture even with zero pan distance. A planned CancelGesture camera intent / epoch bump is not written. Camera input across batches must not lose an active pan solely because its own epoch changed; semantic releases must reject stale contexts.
7. Scope widget focus identity currently omits revision. Review delayed Focus Tasks against replaced modal/document/revision state. Pending structured editors must own keyboard input; a retained background canvas focus must not leak arrows/Space into camera actions.
8. Scenario caller frames already clone Presentation, so they can retain the new pan. Current ScrollRestore receipt machinery still targets the old Canvas Scrollable; migrate actual canvas restoration coherently while preserving Page/outline restoration, exact caller identity, one-shot behavior and raw guards. Process caller restoration/reveal currently targets Scrollable and also needs camera adaptation.
9. Existing scenario FormShown/name focus, one-time marker reveal and staged child-source editor must survive the new stable editor presentation. Keep the same Draft and source_draft state; do not create a second draft, implicit Apply or substitute definitions.
10. Existing workspace/scroll tests target old hosts. Replace obsolete presentation expectations with production-mounted canvas assertions while retaining all semantic/source/history/identity expectations. Do not leave legacy helper-only tests represented as new production acceptance.
## Next bounded work after authorization
Finish minimal BOTH-scene Fill/palette/editor adapters, guarded camera App events, stable caller return and whole-glyph scenario reorder. Exercise real Iced root layout and ordered pointer/key batches at nonzero pan and several zooms, frozen canvas bounds across edit/cancel, stale scope/release rejection, modal keyboard ownership, and exact App source/history/raw preservation. Send a coherent source freeze to root for compile and native gates; this worker remains prohibited from running Cargo/native/Git.
No persistent layout map, core/view schema extension, range wrapping, reconnection expansion, new allocation semantics or B4 changes belong in this slice. Root is compiling the original combined c0e66f9 source independently; do not modify that checkout or equate its results with this WIP. The incoming source_draft essentials-first fix is owned by Codex-9e8r6t; B4 renderer by Codex-ytr83r. Phase 1 is not issue #2 completion.
## Tool outcome
The first mechanical edit command attempted plain python and failed because python was not on PATH; it changed no files. The same intended edits were then applied successfully with the bundled Python at C:/Users/User/.cache/codex-runtimes/codex-primary-runtime/dependencies/python/python.exe. Shared new files were written through apply_patch. No compile, formatter, unit test or native acceptance has run on these changes.
@@ -0,0 +1,62 @@
# Contextual prerequisite creation — implementation and evidence, 2026-09-11
Codex-9e8r6t /root/release_audit. Root-authorized bounded slice following `release-contextual-source-creation-plan-20260911.md`; independent review by `/root/behavior_audit`. Focused contract tests, strict lint, matching Windows GUI build and formatting passed. Root native/integration evidence remains separate as stated below. No native input, commits or release acceptance are claimed by this worker.
## Outcome and contract
An engineer can stage a missing semantic source inside a Scenario Lifeline, process/chain Work contribution, or new State machine owner form. Existing-source controls remain host-owned siblings; the external Codex-956atf reference chooser is untouched. Source and consumer names are independent. Participant kinds are explicit; OA Role is available for the appropriate lifeline only, never a machine owner. Semantic parent is shown explicitly, defaults visibly to the layer root where valid, and has a bounded searchable alternative list. MixedLegacy retains existing-source editing but no generic new-source catalog. Qualified Action creation and inferred allocations/capability involvement are excluded.
Option A from the frozen plan was chosen over suspending the form in Architecture (separate history/orphan risk) and multiplying dedicated source-plus-use tools (would not solve an already partially typed parent). Child Use validates/stages the prerequisite without adopting any model/source change. Back or Escape returns to the retained parent; final parent Apply/Create creates only its actively referenced staged dependency plus consumer and optional machine view in one validated transaction. Choosing an existing reference prunes the inactive source from that transaction.
`behavior/source_draft.rs` captures semantic document identity, typed reference scope, original owner, source name/kind/parent, child invocation token and reserved identities. Parent adapters retain their own raw fields, insertion/branch positions and invocation identity. Private preview Projects use existing core predicates; no staged object enters the accepted catalog. The parent planner's typed CreateElement/CreateRelation/reference/SetSteps records are remapped to reserved IDs, with aliases regenerated from those IDs. Machine/view IDs already reserved by the creation planner remain unchanged. Flattened final commands preserve exact command-correlated consumer selection after successful Document execution. Source write rejection retains both raw forms.
Scenario and process empty prompts count semantic children, not rendered/visible nodes. Scenario first actions carry the current canvas Context and recheck semantic emptiness; process reuses its Context-bound Begin at explicit append. Appearance/resize performs no mutation or focus operation. An unavailable pending owner is rendered as a retained-draft repair/cancel view before choosing any other owner.
## Independent criticism incorporated
- New Work adjacent to an Exchange must match its existing endpoint identity. A newly reserved source cannot do so; preserve the captured slot and refuse rather than retargeting the exchange or silently appending.
- Source Use on an unnamed machine now stages its valid owner and returns, allowing the still-empty parent name to be completed without discarding the source.
- Back is token-validated dismissal and remains available with stale/deleted owners or unrelated recovered raw editors; other source actions keep strict guards.
- Recovered incomplete source kinds have a total summary, followed by a normal planning error rather than a panic.
- Old empty-action events are scoped to their original canvas context. Missing-owner forms remain visible and do not fall through to another scenario/process and panic.
## Verification status
- `focused1.log` / `focused1-status.json`: compiler reported missing Clone for topology's private draft copy. The known-failed rustc was explicitly stopped; Cargo exit 101. No tests ran. Topology Draft/Browser/contextual Draft now derive Clone.
- `focused2.log` / `focused2-status.json`: one guide `explain` call had an extra body argument. The known-failed compiler was stopped; Cargo exit 101, no tests ran. Corrected by merging the explanation body. The focused3 compiler then rejected a test calling a private App method; no tests ran. It was corrected to send the real Message::Command route. The successful focused4 gate includes the nested source-parent, unchanged caller-binding warning and no-op Undo/Redo status regressions.
- `focused4.log` / `focused4-status.json`: PASS22/0,653 filtered out,2.04s execution after9m56s build. Test inventory675. Immutable `tests-verified/arcadia-studio-tests.exe` SHA256 `6E677F58458B6122EA1886AAC98882640CD9CF260651FE3ECE62BDDA4BE38CC0` (107061248bytes); sibling manifest and libunwind SHA256 `F45604A12E8BCD839FD86F667C1BE1CC7C4F9DA2B3C69FF381AC708F0E1AE7D4`. Frozen source inputs: `focused4-inputs.json`. Root executed the immutable full suite: PASS 675/675 in 448.33s; `tests-verified/full-desktop.log` and `full-result.json`. This predates the narrow final origin, guide and lint corrections below.
- Tests target exact source/consumer records, OA/SA strict viewpoints, source/consumer names and aliases, reserved-ID recovery/retry, one Undo/Redo and SourceBundle equality, inactive dependency pruning, second System refusal, Role owner refusal, guarded path insertion and exchange-adjacent refusal, raw source-budget failure, owner-first machine staging, modal Escape, dirty editor admission, recovered inner form, bounded footer geometry, stale CTA, malformed recovery and missing-owner rendering.
- Source formatting and scoped whitespace check passed after the guide correction. Final post-review hardening also rechecks recovered typed source scope against the parent adapter; malformed-scope regression passed in focused4. The full675 snapshot passed as above; the matching final GUI/Clippy gates passed below; real native staged creation and minimum-window readability remain root gates. No Linux runtime or release packaging claim follows from this desktop slice.
Evidence directory: `.local/contextual-source-20260911/`. Frozen older native/creation/packaging artifacts remain unchanged.
## Final origin and compatibility review
After child Back or switching to an existing source, a previously engaged parent must remain bound to its original semantic Project and exact consumer Element, even when no staged dependency reaches the command. Both Scenario and Work source-event admission and final command planning now compare that captured record against the actual parent draft owner. A valid different owner record cannot impersonate it. Missing-origin legacy recoveries bind only on their first valid, previously unengaged child Open; an already engaged recovery never silently rebinds. Back remains token-only dismissal. The worker found the post-Back/inactive command gap; independent reviewer confirmed it and sharpened the different-valid-owner recovery counterexample.
The additional 23rd focused test covers legacy/current recovery × Back/inactive-source for both Scenario and Work, with a valid unchanged control, exact raw preservation on different-Project rejection, missing-origin rejection and borrowed-valid-owner rejection in final preparation and child Open. This final addition passed in focused5: 23/23 contextual-source tests, 653 filtered out, 0.98s execution after 4m48s compilation. Final test inventory is 676. The same compiled test executable also passed guide-final 3/3 in 0.20s. Exact logs and status JSON are retained; strictClippy2 passed all targets with warnings denied in 4m29s; matching GUI and workspace formatting passed on the same frozen inputs.
`clippy1.log` / `clippy1-status.json` completed with exit101 on three `unnecessary_lazy_evaluations` in the empty Scenario first-action buttons. Those constant event values now use `then_some`. No other strict-lint diagnostic was emitted. The stale guide instruction to cancel and create a missing machine owner in Architecture was removed; sequence guidance now identifies work/exchange contributions accurately. These final changes are outside the full675 artifact.
## Owned source and integration boundary
All following source paths are under `crates/oa-desktop/src/`: `app.rs`, `app/checkpoints.rs`, `behavior.rs`, `behavior/creation.rs`, `behavior/scenario.rs`, `behavior/scenario/workspace.rs`, `behavior/topology.rs`, `behavior/topology/contextual.rs`, `behavior/topology/direct.rs`, `behavior/topology/view.rs`, `guide.rs`, and the new `behavior/source_draft.rs` / `behavior/contextual_source_tests.rs`. The contextual topology draft only gains Clone for private parent copies. Root owns integration/index/commits and native input; no core, server, schema/grammar or externally owned reference chooser changes belong to this implementation.
`focused5-inputs.json` records the frozen Rust/build-input scope and current commit context; `focused5-delta.json` records the seven changed desktop files relative to the passing full675 snapshot. It does not claim the uncommitted desktop source is contained in that commit. Immutable final test/GUI staging includes explicit manifests after all final gates and the unchanged-input check.
## Final frozen handoff
Final gate script completed exit0. `focused5.log` passed23/23 (0.98s), `guide-final.log` passed3/3 (0.20s), `clippy2.log` passed all targets with `-D warnings` (4m29s), `gui-final.log` passed the matching GUI build (6m20s), and `fmt-final.log` / status passed workspace formatting. Every gate has a separate started/finished/exitCode JSON. `stage-final.ps1` rechecked every recorded frozen input hash and refused existing output directories before copying. No source changed during these gates.
| Immutable artifact | SHA-256 | Bytes |
| --- | --- | --- |
| `gui-verified/arcadia-studio.exe` | `52DE82E434B636E18DB02D381842AA6DB84B721CFFD5502063F784F37246ACCE` | 112219136 |
| `tests-final/arcadia-studio-tests.exe` | `405577A35CE643AC022885AF58706D6F8B321224AE6D68E2555FBFAF3D626F38` | 107101696 |
| Both stages' `libunwind.dll` | `F45604A12E8BCD839FD86F667C1BE1CC7C4F9DA2B3C69FF381AC708F0E1AE7D4` | 190976 |
Artifact paths are relative to `.local/contextual-source-20260911/`; each stage has `manifest.json`, linked to `focused5-inputs.json` and the explicit seven-file `focused5-delta.json`. The final executable contains676 tests; the full675 pass belongs to the preserved earlier artifact and is not relabeled as a full final676 execution. Only the affected23 and guide3 were run against the final test executable by this worker. Root can run any broader immutable gate without a Cargo build.
Root received the exact final GUI path/hash for independent staged-source native checks. `/root/palette_finish` received the released compiler lease for its separately authorized locked/offline CLI metadata. This worker performed no native GUI actions, release publication, Linux run, index staging or commits. The source/report ownership listed above is frozen; further source changes require an actual new finding and corresponding evidence scope.
@@ -136,3 +136,257 @@ definitions. Those require further explicit semantic intent; they are not
required to validate this staged-source slice. The external chooser remains
owned separately. Implementation review and native evidence follow only after
the author freezes the new source.
## Independent implementation review, 2026-09-11
Codex-9e8r6t `/root/behavior_audit`. Resumed after the author froze the first
production candidate. Read issue #1 through #151 and traced the actual App,
behavior dispatch, nested draft, recovery, view, and command-selection paths.
The author made explicitly identified corrections between the first and second
compile windows. This reviewer edited only this report and ran no Cargo, rustc,
native input, or implementation tests. The author's first compilation exposed
missing Clone derives; no passing runtime result is inferred from that compile.
Five independently derived counterexamples were sent directly to the author
and root. All five have corresponding corrections in the second frozen source
inspected here. The following is source verification, pending the author's
19-case focused gate and root's native acceptance.
| Priority / finding in first candidate | Concrete counterexample and consequence | Correction inspected in second frozen source |
| --- | --- | --- |
| P1: missing pending owner can panic while rendering another owner | Recover source draft for scenario A after A is absent, with another same-layer scenario B present. `behavior::selected_owner` returns pending A, while `behavior::view` falls back to B. B's workspace calls `scenario_context().expect(...)`, but the context still resolves absent A and is None. Process fallback has the same mismatch. With no other owner, the pending form is hidden behind New instead. Safe `sources()` and `prepare_pending()` do not protect this render path. | `behavior::view` now checks the pending owner's presence/kind/layer before choosing a fallback. It shows the retained consumer name plus current child form or explicit parent Cancel. The child Back remains reachable; it never silently opens B as the pending consumer. New orphan-view tests construct both no-choice and remaining-B cases. |
| P1: staged recovery completeness was assumed by presentation | Recover selected staged source with `source.staged.kind = null`, a valid value for its serialized `Option<Kind>`. Deserialization accepts it; `State::label` then calls `unwrap()` and panics in parent view/selection before command validation can return its existing missing-kind error. | `source_draft::State::label` is now total and displays a missing-kind label. Planning still refuses; raw recovery remains intact. A malformed-kind regression exercises both planning and the label. |
| P2: owner-first machine creation cannot leave its valid child | New → Machine, leave machine name blank, Create new owner, choose valid kind/name, Use. The source is valid, but the parent handler calls `next.command(p)` and refuses the blank machine name. Parent edits are locked while the child is open; Back drops the entered owner before the machine can be named. | Machine Use stages the valid prerequisite and checks the actual eligible-owner predicate on its private preview. It does not require an already complete machine. Final Submit still validates the complete batch. The owner-first regression keeps the same source/name while the machine is subsequently named. |
| P2: footer Back disagrees with Escape on retained stale/dirty recovery | After recovering unrelated dirty source/property text, App rejects every source event, including Back. After deleting/changing the owner, scenario/topology validate the origin before dispatching Back; the helper also compares scope first. The visible Back button refuses although global Escape succeeds through `source_back()`. | Token-only `source.dismiss` runs before semantic context checks. App exempts only `Source(Edit::Back)` from other-editor admission. Wrong/old tokens still refuse; Open and Use keep their guards. Added dirty-editor and deleted-owner Back regressions preserve parent/property state. |
| P2: new empty actions accept a previous owner's event | Capture A's new first-action message, navigate to clean B or another layer, then deliver A's bare `Start(owner, tool)`. That route checks dirtiness and owner existence but carries no context. It can open A; `selected_owner` prioritizes the resulting pending owner. | Scenario CTAs now use `CanvasEvent::First(Context, Tool)` through the existing context and dirty guards, with a fresh semantic emptiness check. Process first Work uses its existing context-bound Begin with the explicit default insertion. A new owner/layer-stale CTA regression rejects old context and admits the current one. |
The source changes address the reported mechanisms. Their tests are authored
regressions, not independently executed evidence in this review. In particular,
constructing an orphan view prevents the identified `expect` panic but does not
by itself prove its footer can be clicked at the minimum native window size.
The unchanged core contracts are used correctly in the inspected paths:
- App form start/source admission checks raw source and property dirtiness
without calling generic Apply. Source/parent submission retains the existing
dirty guard. Model-menu and modal Escape precedence is resolved before child
Back, then parent cancellation. Raw edits and form rendering create no
accepted model/history operation.
- Source commands keep source name, semantic parent, kind, layer, document, and
exact captured consumer/parent identity distinct from the consumer. Existing
validation admits Role only where appropriate, enforces System uniqueness,
and preserves allowed completeness warnings. No allocation, role realization,
reuse binding, or shared exchange retargeting is inferred.
- For an actively selected staged source, the helper previews the source,
prepares the original consumer, remaps every new element/relation identity
and typed reference plus SetSteps to the reserved set, then forms one flat
validated batch. Recomputed aliases use the reserved IDs rather than an
abandoned temporary UUID. Machine/view/placement identities remain reserved
by the existing creation planner. This review does not claim that the older
non-staged planners reserve their IDs across separate preparations.
- App compares the complete prepared staged command before assigning its
consumer selection, and assigns it only after `Document::execute` succeeds.
Parent acknowledgement follows success. Choosing an existing reference makes
the staged source inactive: preview/combine omit it, so it is not published.
Parent Cancel drops the entire pending state.
- Captured process link identity/endpoints and presentation-before slot remain
validated. Work insertion preserves the existing guarded link and adds the
new successor link; a fresh source next to an existing Exchange still cannot
impersonate the required endpoint and refuses with raw input retained.
- New empty guidance counts semantic Lifeline/Occurrence/contribution records,
rather than rendered positions. An unresolved lifeline is still a lifeline;
ScenarioUse, fragment, and offscreen content do not become empty merely
because of presentation. Existing scenario canvas/scroll slots remain in the
surrounding workspace tree while the explicit child form occupies its form
pane.
The author deliberately made explicit staged-consumer names exact, including
leading/trailing whitespace; source names are also exact, and a blank consumer
continues to default to its chosen source name. This supersedes the earlier
matrix's inherited trimming expectation **for active staged-source consumers**.
The older existing-reference consumer path still has its pre-existing trim
policy. Native/oracle expectations must distinguish those paths and must not
borrow an observed output name to define the expected model.
Second frozen source fingerprints captured at approximately 11:39 UTC:
| File | SHA-256 |
| --- | --- |
| `crates/oa-desktop/src/app.rs` | `81E0AA21A93126D716E628B3EBD440201AB19D6B578E2585F5E500072244D58B` |
| `crates/oa-desktop/src/behavior.rs` | `A0E595F17F254002F1FBD7984EEB679320DAB042CA310C38D8916F0055713780` |
| `crates/oa-desktop/src/behavior/source_draft.rs` | `32057E30C0526FA674D463F47C4EAC7C0F391B1F4C5060757981A3CBED16C6AE` |
| `crates/oa-desktop/src/behavior/scenario.rs` | `759805631BE9E8526DACFB959D91C76CA1A2A40F47F8645BCEB0E0C15B77E326` |
| `crates/oa-desktop/src/behavior/topology.rs` | `8E1C65AFE4C7C097F683B33A6DF237219F48AFD1E81F81329B07612F4D6994CC` |
| `crates/oa-desktop/src/behavior/creation.rs` | `BE75840293E84FCAE90DC6D17CCD6630A586B389FB189ADF73D1E9B046CE3DE2` |
| `crates/oa-desktop/src/behavior/contextual_source_tests.rs` | `242B68E0DFF14E57E1F510EA3DEA95ED1B2ADCDABFD24C9F493FC871CB50F049` |
No additional implementation blocker was found in this bounded review. Required
evidence still includes terminal focused checks and native minimum/wide form
authoring, plus independent complete model/source comparison for actual
source/consumer creation and Undo/Redo/reopen. The earlier explicit detached-file
retention rule remains unchanged. No native result, full-suite result, or release
completion is claimed here.
## Final staged-source delta review, 2026-09-11
Codex-9e8r6t `/root/behavior_audit`. Reviewed only the changes after the recorded
second freeze: compact consumer captions, explicit creation summaries, recovered
typed-scope validation, nested semantic-parent and callee/caller regressions,
guide correction, and App Undo/Redo result handling. Read the final author gate
and current Gitea decisions through #178. No production edits, Cargo, rustc or
native input; root retains the immutable full-suite run and native acceptance.
The source inspected in this pass matched all ten corresponding entries in
`.local/contextual-source-20260911/focused4-inputs.json`: App, behavior, source
draft, scenario, scenario workspace, topology, topology view, creation,
contextual-source tests and guide. This identifies the precise compiled source
without presenting the earlier second-freeze fingerprints as current.
### Concrete corrections identified at the final boundary
1. **P2 — The State machine guide still directs the user out of the new flow.**
At `guide.rs:547`, the first step says to cancel and create a missing owner in
Architecture; the immediately following new step describes Create new owner
with the retained machine draft. The old sentence would unnecessarily lose
the user's pending setup and hides the new supported route. Sent directly to
the author/root; the author removed it after the Clippy source freeze and
this reviewer confirmed the source readback. The guide's previously rejected
`explain` argument count is already corrected in the passing focused4 source.
2. **P2 — The source origin must survive inactive dependency planning.**
The author identified this additional case before review closure, and this
reviewer independently confirmed its actual code path. Start a Lifeline or
Work form with a valid existing target, open Create new source, then Back.
`source.engaged` remains true, but there is no editing/active staged source.
`check_scope` and `preview` therefore pass without a source-side document
check. Parent `Draft::command` at scenario.rs:1350/topology.rs:582 then reaches
its ordinary consumer planner against a different `Project.id` with the same
owner and target UUIDs. The captured `source_origin` is checked only while
handling Source events. Use followed by reselecting an existing source has
the same inactive-dependency mechanism. This is a concrete preparation-boundary
failure; no claim is made that ordinary file navigation preserves the form
through its existing App reset.
The narrow proposed correction checks captured document plus exact owner at
final command preparation whenever source creation has been engaged, keeping
token-only Back and cancellation available. Regression must begin with a
valid existing target, prove baseline preparation succeeds, then refuse the
other document while retaining all raw fields. A compatibility edge was sent
separately: old recovered forms legitimately have `source_origin=None` and
unengaged default source state. Their first successful source Open should bind
the current origin; an already engaged missing/mismatched origin must never
be silently rebound. The author implemented these document/legacy controls
after the source freeze, with a valid-target Scenario/Work × Back/inactive ×
ordinary/legacy regression matrix. That later source is not covered by
focused4 and no new execution is claimed here.
**One final owner-binding detail remains at this handoff.** The initial new
guard compares the captured owner with `p.elements.get(&owner.id)`. A recovered
origin can contain another valid, unchanged same-layer owner B while the
parent draft still targets A. That lookup validates B against itself; the
otherwise valid consumer planner still operates on A. Compare the captured
owner record with `p.elements.get(&self.owner)` in final planning and
`p.elements.get(&d.owner)` in Source-event admission, or explicitly require
the two IDs to agree. The simplest negative control replaces only the
serialized origin's owner record with B, leaving the valid target/slot and
actual draft owner A unchanged. This avoids a false rejection caused by an
unrelated invalid Scenario parent slot. This reviewer sent that independently
derived detail to author/root; final strengthened source readback is pending.
### Confirmed scope of the other deltas
- New outer consumer captions call `views::short` with 60 characters, or 80 in
the orphan card. That helper collapses whitespace and truncates presentation;
it does not rewrite raw draft names. The source fields still have their own
remaining-height scroller and a separate Back/Use footer. Active-source
summaries describe the actual source+lifeline/work/machine transaction, with
the first machine view explicitly conditional. No view call adopts a model
command. Native minimum-window readability is not inferred from this trace.
- `source.check_scope` is called before preview in all three parent planners.
A selected staged source or current child with the wrong typed consumer scope
is rejected; an inactive staged source remains excluded. The malformed-kind
regression now also falsifies a Machine scope inside a Lifeline draft.
- The nested-parent test uses a real OA Activity parent, refuses an Entity as
work ownership, stages exactly one child Activity, then changes the captured
parent's kind and proves private preview refuses without changing the raw
source state. The implementation validates both the exact parent snapshot
and current `can_parent` predicate. This is not a test of every parent family.
- The callee regression has one existing ScenarioUse with an empty binding list.
Adding a new Activity-backed callee lifeline preserves every original
element, relation and diagram and produces the expected
`missing_lifeline_binding` warning on that use. Its shared history helper
checks model/source Undo, Redo and fresh source reconstruction. This is useful
evidence for allowing an incomplete caller; it is not a claim of independently
testing multiple callers with nonempty Role mappings.
- App Undo/Redo now uses the actual boolean returned by `Document`, calls sync
only for a history step, and reports Nothing to undo/redo when appropriate.
Its test exercises both an empty history and a real rename through the public
`Message::Command` path. Existing `apply()` guards for pending behavior/source
remain in front; the existing property-Apply behavior is unchanged. No claim
that all Undo/Redo requests are inherently free of property application is
introduced.
### CLI documentation cross-check
`docs/source-cli.md` and README's explicit `cargo run --locked -p oa-cli --bin
arcadia -- check examples/text-first/project.arcadia` example match the final
CLI adapter. The example's execution was already verified by root and was not
repeated here. The page correctly separates aliases from display names, previews
from explicit per-invocation writes, changed-file reports from unchanged captured
dependencies, pending recovery from automatic recovery, and source3–7 migration
from canonical formatting or detached-file deletion. Root accepted and applied
two precision additions during this review: Unicode scalar columns, and
`written:false` not implying guaranteed rollback. Readback confirmed both. No
new CLI implementation issue or saved-plan/digest requirement was raised.
### Evidence status
Read the author's terminal `focused4.log`/status: **22 passed, 0 failed, 653
filtered**, 2.04 seconds test execution, exit 0 at 11:59:25 UTC. The matched
immutable test executable is SHA-256
`6E677F58458B6122EA1886AAC98882640CD9CF260651FE3ECE62BDDA4BE38CC0`, inventory 675.
The full suite was running under root at review time; it is not reported passed
here. Guide/source-origin corrections above are later deltas and require their
own coherent source/test evidence, including the final owner-binding detail.
Current source-input manifest, focused logs,
and author's revised implementation report remain the build evidence authority;
this reviewer executed no desktop tests or native workflow.
### Final source readback closure
The author froze the strengthened source and root requested a short final
readback. **No remaining material source finding in this bounded delta review.**
This closes the pending owner-binding detail above; it does not assert that the
new source has completed its later compiled/native gates.
All four guards now compare the captured owner record against the actual parent
draft's `d.owner`/`self.owner`, in addition to the document identity. The 23rd
regression includes ordinary/legacy × Back/inactive-existing variants for both
Scenario and Work. Each has a valid existing-target control, wrong-document and
null-origin refusal, and a borrowed full record from another valid same-layer
owner. The latter must fail both final preparation and Source Open while the
entire raw recovery snapshot remains unchanged. This is the exact falsifier
requested in the preceding handoff, not an unrelated invalid-parent failure.
For an old unengaged draft, first Source Open creates its missing origin only in
a private clone; the clone replaces the parent only after source admission
succeeds. It cannot rebind an already engaged missing/mismatched origin. Back is
still dismissed by its token before those semantic guards, and parent Cancel
remains available. The old guide sentence is absent and the retained-owner
creation instruction is present.
Readback hashes match the author's final freeze:
| File | SHA-256 |
| --- | --- |
| `behavior/scenario.rs` | `D12A4B1CAA61C08F69C4D2A64BD15568C4B7158DEA91AAC5A52A58A35ADB863C` |
| `behavior/topology.rs` | `D06B2F0FC39B4814E94200297E148F1D4D97E04CB167766030AC9DF18689E3B0` |
| `behavior/contextual_source_tests.rs` | `5F05318A6533503B9D6FEBF1EC4DD7256F192394BC1B952A7904C8175BD8DDAE` |
| `guide.rs` | `47EEB3CC0A48D5712720096015A08387CD354FB91559B0BD6FA106B5CAC6292C` |
Paths in this last table are under `crates/oa-desktop/src/`. Root subsequently
reported the earlier immutable 675-test artifact passed 675/675 in 448.33 seconds
(Gitea #185). That artifact predates these final corrections. The author's
focused5, matching GUI/lint and root's native/source oracle remain the appropriate
next evidence; no Cargo, rustc, native input or production change was performed
by this reviewer during closure.
@@ -117,4 +117,33 @@ The revised candidate has **13 tests prepared** at the worker handoff. No produc
### Second executed gate — root integration checkpoint
Root ran the corrected candidate through `cargo test --locked -p oa-desktop forms::reference_chooser::tests::`. The log `.local/logs/reference-chooser-second-20260911.log` records a warning-free compile in 1m47s and **13/13 passing**, zero failures/ignored, 642 filtered out, in 0.41s. This includes actual mounted clipboard paste and Unicode IME commit alongside the corrected physical-key fixtures. The module is declared under `#[cfg(test)]` only. This result establishes the isolated widget/state tests; the complete inspector/review adapters, native font/focus/geometry, responsiveness and model/source/history acceptance remain open.
Root ran the corrected candidate through `cargo test --locked -p oa-desktop forms::reference_chooser::tests::`. The log `.local/logs/reference-chooser-second-20260911.log` records a warning-free compile in 1m47s and **13/13 passing**, zero failures/ignored, 642 filtered out, in 0.41s. This includes actual mounted clipboard paste and Unicode IME commit alongside the corrected physical-key fixtures. At that checkpoint the module was declared under `#[cfg(test)]` only. This result establishes the isolated widget/state tests; the complete inspector/review adapters, native font/focus/geometry, responsiveness and model/source/history acceptance remain open.
### Production adapter candidate — source handoff, 2026-09-11
Codex-956atf /root/behavior_audit implemented the coordinated Inspector and active Review integration after the source freeze was released. This new candidate has **not yet been compiled or executed**. The prior 13/13 result above does not validate these changes.
- `forms.rs` now declares the chooser in production, renders live scalar reference fields through `reference_chooser/adapter.rs`, and maintains transient draft/session generations. Existing `reference_ids` remains the eligibility authority. A Choose/Clear rechecks compatibility and replaces only its authorized subtree with `shape_scoped`; invalid sibling text and inactive constructor caches remain staged. Historical baseline paths retain the historical editor and never enter the catalog. Recovery encoding is unchanged.
- New `app/reference_authoring.rs` owns one transient popup and validates document instance/revision, page/layer, exact draft generation, and current Inspector or Review host. `App::view` keeps the wrapper mounted at the root. Small Message/update hooks route browser events before generic modal handling and invalidate obsolete hosts after ordinary changes. Captured search shortcuts cannot save, apply, navigate, or undo a model. Escape closes this popup before closing its owning review.
- `semantic_review.rs`, `row.rs`, and `batch.rs` carry exact addressed Open/Event/Set messages. Browse/search/page/cancel stop at the app controller, without invoking Review update/replanning. Writes require the currently active row, selected Candidate facet, authorized root, and the exact scalar/group/attachment draft. A delayed banked-row message is inert. `semantic_review/view.rs` adds one focus scope around the active candidate detail.
- `reference_chooser/trigger.rs` implements the compact focusable trigger, Enter/Space opening, form-local Tab traversal, and generation-scoped Escape/Tab return. An asynchronous focus operation first finds its exact field; a removed/replaced field causes no focus changes. Source inspection caught an anchoring defect before compilation: Iced child updates use scroll-content coordinates. The root popup now measures field bounds through an operation that accumulates nested scroll translations and clips to visible scroll viewports before each input. This also handles scroll then activation without relying on a paint to update coordinates.
Nine new tests are prepared under `app/reference_authoring/tests.rs` and `tests/mounted.rs`: literal 1,000-entry eligibility/search and scoped writes with invalid raw text; exact accepted Project/source and Undo/Redo after explicit Apply; invalid/missing/incompatible current retention and Clear; recovery/document/revision/source/modal/context invalidation; array reorder/constructor replacement; scalar/group/attachment Review routing and no replan during browsing; stale active-facet and banked-row rejection; real mounted trigger→App→root-overlay keyboard flow, same-batch query/Escape, adjacent-field Tab return, stale focus, and scrolled pointer/keyboard anchors. Existing 13 isolated expectations were not weakened. These are prepared assertions, not passing results.
Root gate: format and compile current source, then run `cargo test --locked -p oa-desktop app::reference_authoring::tests::` and `cargo test --locked -p oa-desktop forms::reference_chooser::tests::`; include existing forms/history and semantic-review regression suites, strict Clippy/all-targets, and the combined desktop suite before native staging. The worker ran no Cargo/native commands or commits. `git diff --check` was clean at handoff; root's separate ScenarioCanvas FormShown hook is preserved.
Open native gate: the actual Inspector and Review popup at 1100×700, scrolled forms, repeated names and long containment/UUID detail, keyboard focus/Tab, pointer cancellation and overlay priority, real text/font clipping, and measured 1,000-entry query/eligibility latency. The new host tests use actual retained widgets with the null text renderer; they do not establish native typography, accessibility, touch usability, or release acceptance. Other specialized multi-member/reference-mapping controls remain their existing bounded workflows; this slice replaces scalar live-reference property choosers in both hosts.
### Independent focus counterexamples and correction — source only
The independent production reviewer found concrete P2 keyboard failures in the initial host candidate: local Tab enumerated offscreen fields without revealing them; a fully clipped reference then had no anchor and Enter silently failed. Fresh forms could not acquire focus through Tab, and traversal past the final field left no focus from which to continue. The review also identified stale return tasks that could locate an unchanged Inspector field underneath a newly opened Review modal, and background focus that could receive ordinary characters after modal reference-trigger acquisition or outside dismissal. These findings came from installed Iced and application source inspection, not native execution.
Under a renewed desktop-only lease, focus traversal moved to the stable root Popup. App supplies the exact active form session and whether it owns the Review modal. New `reference_chooser/trigger/focus.rs` collects only that form's focusable controls, tracks enclosing scrollable geometry, and reveals the selected destination from the innermost viewport outward. Fresh Tab/Shift+Tab enter at the first/last field; boundaries cycle within the form. Ordinary Inspector entry respects an already focused control outside the form. Review entry clears background focus, and the modal root enforces that ownership before ordinary input as well, including pointer opening and outside dismissal.
Asynchronous Escape/Tab/Choose return still requires the exact generation-scoped originating field, and now also reads the root's current form owner before applying focus or scroll. A different modal, a new chooser invocation, or a retained old Inspector underneath Review makes the old operation inert. The active Review form is exposed by one small `batch.rs::reference_form` accessor; no scenario/workspace hook changed.
Four additional mounted tests are prepared, bringing the new App/host suite to **13 tests**: initial offscreen fields with one and two scroll containers, Tab-to-reference/Enter and return reveal; fresh entry and repeated boundaries in both directions; delayed Inspector return while its unchanged field remains under a Review Stack/Opaque; and actual Review pointer opening, Outside dismissal, then a character key with zero leaked messages or raw/model/source changes. Existing literal model, source, raw, history and isolated-widget expectations remain. No compile/native result is claimed for this corrected candidate. Root retains formatting, first combined compile, focused suites and native gates.
A subsequent independent read-back caught an overbroad ordinary-input rule: clearing focus outside the Candidate form would also deactivate Review's legitimate engineering-rationale input, which is a sibling above that form. The correction now uses two distinct widget boundaries. `trigger::modal`, applied to the complete `batch::State::view`, marks the whole owning Review dialog without changing its layout. Ordinary-input cleanup preserves all controls inside that modal. The narrower draft/session boundary continues to define explicit Tab and return-to-field traversal. Explicit focus transfer can deactivate previous controls as usual; mere typing does not clear a sibling inside Review.
The new regression starts from the existing source7 interface-operation fixture, adds an OA occurrence counterpart, enters actual SA-to-OA Review with an operation-omission rationale and an active Candidate reference facet, then types the complete reason through the mounted sibling input outside the Candidate scope. It requires one correctly addressed rationale event per physical key, retained focus, unchanged candidate/Inspector raw buffers, unchanged accepted source and no history entry. The modal fixture used by the prior dismissal tests now includes this complete modal boundary. The App/host candidate suite now has **14 prepared tests**. The independent finding is source-corrected; execution and native acceptance remain pending.
@@ -0,0 +1,79 @@
# Independent production reference chooser review — 2026-09-11
Reviewer: Codex-956atf `/root/semantics_critic`. Scope: Inspector and active semantic Review scalar live-reference chooser; read-only production inspection. The reviewer did not run Cargo, the desktop application, or native input, and did not edit production, prior reports, or Git state. Root owns integration and execution; `behavior_audit` owns corrections. Scenario FormShown and canvas scrolling are separate work.
Status: the three concrete P2 findings below are corrected at source-review level after independent read-back. No remaining source blocker was found in the assigned chooser slice. The corrected host suite is still uncompiled/unexecuted at this review; this is not production or native acceptance. Gitea issue 1 comments 209 and 219 record ownership and the initial findings.
## Findings in the first production candidate
These locations refer to the formatted candidate inspected before the focus correction: `forms/reference_chooser/trigger.rs` SHA256 `529C36791272346C166064C02BB659FD0BE6C2A76376CC6C5D440B6BA22495F7`; `app/reference_authoring.rs` SHA256 `A8FEA3D252C01C849CE5B0ADE4FBBB9D3CF3A34E58C062FBEBF152710377F8E8`. Paths below are relative to `crates/oa-desktop/src` unless stated otherwise. They are source-derived falsifiers, not executed failure logs.
### K1 — P2: keyboard focus can enter a clipped field that cannot open
`forms/reference_chooser/trigger.rs:185` runs ordinary Iced focus traversal across all focusable descendants. `FromField`/`SetFocus` around lines 408–449 also select by traversal index alone. Neither reveals the selected field. The root `Measure` correctly clips reference anchors to nested viewport intersections at lines 357–391, and activation at line 236 publishes an Open only when such an anchor exists.
Minimal falsifier: put the reference trigger immediately below a 180-pixel form viewport; focus the last visible text input, press Tab, then Enter. The reference becomes focused below the viewport, but its anchor is `None`, so Enter publishes no chooser. Text inputs below the fold can likewise receive unseen typing. Escape/Tab return can select an offscreen neighbor. The original mounted scroll test snaps the reference into view before interaction, so it does not test this sequence.
The installed Iced 0.14 `widget/operation/focusable.rs` implements focus selection and traversal without using scroll geometry; there is no implicit reveal to complete this workflow. The author confirmed the defect. Resolution: select within the active form, minimally reveal the destination through its enclosing scrollables, and remeasure before activation; add a mounted test without a preparatory scroll.
### K2 — P2: fresh/boundary Tab loses form entry and can reach an inactive host
The same scope branch handles Tab only if `Count.focused` is true (`trigger.rs:185`). Iced `focus_next` unfocuses the last item and `focus_previous` unfocuses the first. Subsequent Tab sees no focused child and does nothing; a newly mounted unfocused form cannot enter its first field through this handler. Custom chooser return at `trigger.rs:430` also selects `None` beyond either boundary and clears all focus.
Minimal falsifiers: mount an unfocused form and press Tab; or focus its last field and press Tab twice; or close a chooser on its final field with Tab. No active field is recovered. Additionally, opening Review while Inspector retains focus leaves two mounted scopes: Iced `opaque` captures only mouse presses (`iced_widget-0.14.2/src/helpers.rs:660–682`), and `Stack` stops descending only after event capture (`stack.rs:231–263`). An unfocused Review can therefore let Tab reach the underlying Inspector. This is a real host-routing consequence of the same missing active entry policy, not a claim that an obsolete chooser passes the App's write guard.
The author confirmed the defect. Resolution: put keyboard routing at the stable root with the exact active form session, explicit first/last entry and boundary policy, and a distinction between a modal-owned Review form and an ordinary Inspector alongside other editable controls. Regression coverage must include an inactive mounted scope.
The correction must also prevent simultaneous inactive text focus: if modal entry focuses a Review reference trigger while an underlying Inspector text input stays focused, a character key unhandled by that trigger can pass through `Stack`/`opaque` into Inspector. Check retained Inspector raw text after modal entry, reference focus and character input, including chooser dismissal. Correct Tab routing alone does not prove general keyboard isolation.
### Additional ordering case required by the correction
An asynchronous Escape/Choose focus Task can be created while Inspector is active, then run after Review or another modal opens. The same Inspector field and generation can remain mounted underneath that modal. A traversal that only proves target existence can still focus the inactive host. Require current root ownership as well as exact field/session/generation before applying return focus. The original stale-focus test replaces the generation, so its target disappears and does not establish this stronger condition. The author has received this concrete test case.
### K3 — P2: the first focus correction disables a legitimate sibling Review input
The first correction's `Popup::update` calls `own_input` before every input while Review owns a Candidate form. Its `focus::exclusive` clears every focusable outside that form. This protects Inspector but also clears controls inside the same Review modal. A concrete existing control is `semantic_review/view.rs:333`, the required engineering rationale for explicit SA→OA technical-operation omission. It is mounted above `facts_view`, outside the Candidate detail scope around line 873.
Minimal falsifier: review such an Exchange occurrence in SA→OA direction, enable operation omission, select a Candidate facet, click its required rationale, then type a character. `own_input` unfocuses the rationale before dispatching the character, so its required text cannot be entered. The first correction's modal fixture contains only the reference trigger, so it cannot detect this regression. Resolution: distinguish the entire owning modal from the narrower field/form scope, clear only background focus, and add a mounted sibling-input regression. This finding was sent to the author and root at correction read-back before acceptance.
## Source checks with no counterexample found
- `app/reference_authoring.rs:52–89` checks Inspector versus exact active Review ownership and binds opening to document instance, project identity, document revision, page/layer, draft session, generation and live path. Unrelated modal, hosted, source-dirty, pending and busy states invalidate the chooser. This prevents the keyboard defects above from becoming authority to write an obsolete draft.
- `semantic_review/batch.rs:299–365` requires the current active row and Facts tab, checks the frozen project, rejects unaddressed writes and stops Open/Event messages before normal update/planning. `semantic_review/row.rs:529–545` requires the selected Candidate facet and authorized path roots, then resolves the correct scalar/group/attachment draft. The App validates again before mutation.
- `forms/reference_chooser/adapter.rs` derives candidates from the existing typed `reference_ids`, excludes nil and historical paths, retains invalid/missing/incompatible current text, and rechecks actual eligibility and optionality immediately before a scoped setter. Candidate metadata covers the actual `Project::identities` variants; the chooser does not introduce a second eligibility authority.
- The App's `Outcome::Browsed` branch at `reference_authoring.rs:140–144` restores only transient chooser state; it does not call the Review planner or the draft setter. Only explicit Choose/Clear uses `shape_scoped`, preserving unrelated raw inputs and constructor caches. Apply, accepted Project/source and history remain their existing separate boundary.
- The popup is persistently mounted above the ordinary view/context-menu wrapper and below hosted UI. Browser input is captured; the controller blocks global save/undo/open/new/source-navigation/guide shortcuts while open, and Escape closes the picker before its owning Review. Absolute row actions are invocation/result stamped; relative navigation/query ordering is intentional. Press and release must match the same pointer/action/stamp before a row write.
- The original root measurement traversal matches installed Iced's `Scrollable::operate` call order: each scrollable reports its offset immediately before child traversal. It accumulates nested offsets and viewport clipping with stack restoration. No coordinate contradiction was found by source inspection. The prepared mounted test establishes only a single already-scrolled case when executed; nested/clipped/full-app behavior still needs evidence.
## Evidence boundaries
The earlier 13/13 isolated chooser tests passed under root's test-only declaration and do not establish the new host adapter. The first production handoff adds six App tests and three retained-widget tests, initially unexecuted. They cover 1,000-entry eligibility, literal invalid sibling preservation, source/Project/Undo/Redo after Apply, invalid current/Clear, stale contexts, constructor/array changes, Review scalar/group/attachment behavior, banked rows and mounted opening/return/scrolling. The mounted renderer is a null text renderer with a purpose-built form, not the actual complete Inspector/Review view.
The correction must be compiled and its new focus/reveal tests executed by root, followed by the relevant existing forms/history/Review gates. Native acceptance remains necessary for actual font clipping, long details, 1100×700 layout, nested scrollers, modal priority, keyboard focus, gesture cancellation and measured 1,000-entry query/eligibility latency. This report does not turn authored tests, source reasoning, or the earlier isolated pass into production/native evidence.
## Corrected-source read-back
| Finding | Resolution independently inspected | Evidence still required |
| --- | --- | --- |
| K1: clipped focus | `trigger/focus.rs::Collect` retains enclosing scroll geometry, chooses the exact form destination, computes bounded reveal from inner viewport to outer viewport, then applies scrolling and focus. Root measurement runs again before Enter activation. | Execute the prepared fresh offscreen, one/two nested scroll, Enter, and return-after-scrolling-away test. The test starts at zero scroll and checks the reference really begins below the fold. |
| K2: entry, boundaries and ownership | Root Popup receives the current App form session. Fresh Tab/Shift+Tab enter first/last; boundaries cycle. Ordinary Inspector traversal respects another focused control. Exact asynchronous return requires both current root ownership and the original generation-scoped field. Review deactivates background input before ordinary input, including pointer opening and Outside dismissal. | Execute the bidirectional cycles, still-mounted old Inspector under actual Review ownership, character non-leak and Outside/no-focus-task tests. |
| K3: sibling rationale | A separate layout-neutral `trigger::modal` boundary wraps the entire `batch::State::view`, including header, rationale, facts and footer. Ordinary-input cleanup preserves everything inside that modal; Candidate form identity remains the narrower explicit Tab/return scope. | Execute the added SA→OA operation-omission rationale test and confirm the actual full view in native use. |
The final mounted rationale fixture loads a real operation-bearing SA occurrence, creates an OA counterpart, opens reverse Review, enables omission, and activates a Candidate parent facet. Its rationale is inside the modal but outside the Candidate form. It asserts every character emits the correct addressed rationale event, the final literal is `Engineering reason`, the rationale retains focus, and Candidate recovery, Inspector raw input, SourceBundle and Undo remain unchanged. The Inspector-under-Review and Outside fixtures likewise retain the actual old input and use real Stack/Opaque plus the reference adapter; they do not substitute a no-background modal.
The corrected host suite contains **14 prepared tests**: six App tests and eight mounted tests. None was run by this reviewer or the author at handoff. The separate earlier isolated **13/13** remains exactly that earlier executed checkpoint. Installed Iced Winit source also confirms that synchronous App updates rebuild user interfaces before returned widget operations (`iced_winit-0.14.0/src/lib.rs` around 1214–1251), supporting the new-generation/root-owner return design; it does not replace runtime acceptance.
Root can proceed with the compiled integration gates against this corrected candidate. A compile/test failure or native focus/geometry failure must be resolved before treating these findings as execution-closed. No redesign of typed eligibility, raw staging, semantic planning, or source/history contracts is indicated by this review.
Final correction hashes were independently read from disk and match the author's final freeze. Paths remain relative to `crates/oa-desktop/src`:
| File | SHA256 |
| --- | --- |
| `forms/reference_chooser/trigger.rs` | `4932CCE196DB20B7EC444C42AB01B3217D9B9AA0B9203D0E7466759B04722552` |
| `forms/reference_chooser/trigger/focus.rs` | `C59FC25096829B86632BE78979F54178D28653FB1E7447E5122FC5C1243456AE` |
| `forms/reference_chooser/widget.rs` | `D380A574F381C0AA6A5CEABC57F5879487623C816C120F2C3ADFE4C7737F03DC` |
| `app/reference_authoring.rs` | `81CFC1057082451C70E12DB3F5DCDE9FEC820F2B101592894B6CBAF132A5013C` |
| `app/reference_authoring/tests/mounted.rs` | `D50F89A1998DD6B37C841D63B33C1F72CD844FEB3CEA2F75C9294D3935416F9E` |
| `semantic_review/batch.rs` | `29F498265FC12C0B33B8186911ECC4D7C7DC806CC36BCBA51862436EAB1E12A1` |
These identify the inspected source before root formatting/build. They do not identify an executable. The author reports a clean `git diff --check`; the reviewer checked only the new review artifact for whitespace and preserved all frozen reports and production files.
@@ -0,0 +1,25 @@
# Independent staged-source authoring evaluator preparation
Codex-9e8r6t /root/palette_finish,2026-09-11. Root authorized new `harness/staged-source-authoring-contract.rs`, `harness/fixtures/staged-source-authoring/` and this report. No native input, production/desktop edits, Cargo or direct-rustc compilation occurred in this preparation. The desktop implementation owner confirmed that the three selected workflows match the published source/consumer contracts; no created output was consulted.
The frozen baseline and exact native steps are in the fixture README. Two explicit empty owners keep the three cases focused: an OA OperationalActivity scenario and an SA Functional chain, followed by creation of a new SA actor-owned machine with opt-in view. Activity/lifeline and Function/contribution use separate exact names. Source ownership remains layer-root; lifeline/occurrence ownership remains the selected behavior. No implicit allocation, capability involvement, extra occurrence, sequence link or view is predicted.
The standalone evaluator constructs complete expected records and validated command batches using only `oa_core`: source+lifeline; source+Action+SetSteps; actor+machine+diagram+Layout. The published first-view geometry is independently specified. The exact source updater supplies expected file placement, includes and trivia retention. Only fresh role UUIDs are discovered; deliberately false role-carrier metadata/geometry in self-check must not contaminate expected fields.
The `check` command takes a separate original first-commit role carrier for every cumulative stage. Earlier role identities cannot be borrowed again from a later stage's output. Every supplied carrier is itself checked against the appropriate complete expected Project and SourceBundle. Ordinary disk file sets are exact; Undo uses the corrected explicit-origin retained-file rule with no arbitrary detached-file allowance. Reopen additionally supports direct exact comparison against the first accepted/Redo checkpoint.
Existing immutable CLI `BAD169F8…E3559` ran read-only `check` on the new baseline: exit0, no source writes, exactly three expected warning records (`missing_system`, two `empty_sequence`). Artifact `.local/staged-source-oracle-20260911/baseline-cli-check.json` and its command/exit record preserve that admission result. This verifies the authored fixture's current-source admission only; it does not compile or validate the standalone oracle or accept staged-source creation. Baseline hashes are recorded in the fixture README and embedded source strings will pin exact bytes in the eventual evaluator executable.
Evaluator self-checks are written but **not executed**: semantic/source fixed points, duplicate command rejection and unchanged model, core-session one Undo/Redo, exact untouched baseline/comment preservation, source binding/owner/geometry falsifiers, prior-stage identity replacement refusal and known/unknown/changed/missing retained file cases. Root must grant a matching-rlib direct-rustc window before any pass is claimed. No native or full release acceptance follows from preparation.
## Subsequent authorized compilation and evaluator checks
Root granted a short direct-rustc window before the desktop GUI/Clippy gate. Verified the designated core rlib `.local/target-gnullvm/debug/deps/liboa_core-3e81c616fea1d901.rlib` SHA256 `5018A1FD7286ED9DFEDCBBF5B68B399AD5D143F2A9AAAED3078CFC934C9589C4`, initialized the existing toolchain and ran:
```powershell
rustc --edition 2024 -D warnings --crate-name staged_source_authoring_contract harness/staged-source-authoring-contract.rs --extern oa_core=.local/target-gnullvm/debug/deps/liboa_core-3e81c616fea1d901.rlib -L dependency=.local/target-gnullvm/debug/deps -C linker=clang.exe -o .local/staged-source-oracle-20260911/staged-source-authoring-contract.exe
```
Compilation passed in **7.566s**, finished **12:01:48 UTC**. Compiler lease was immediately returned to the desktop owner before isolated runtime checks. Both `self-check BASE` and `check baseline BASE BASE` passed. All previously listed evaluator falsifiers ran, including stronger separate prior-stage identity pinning. Expected stages have3/4/6 reachable files; baseline has2. This supersedes the preceding preparation-only status without implying any produced native-output acceptance. No generator is necessary: the fixed source fixture itself is the input.
Executable: `.local/staged-source-oracle-20260911/staged-source-authoring-contract.exe`,20864512 bytes,SHA256 **`967D9BA3C6FB3A7EFB410833903EC94556CA2D48A67D5072C8BB160F40A00F9C`**. Source SHA256 `9EB9DD3868B6CB772F907D16AE02995AD10A16F85C087012C24948E6E6FE775D`. Runtime evidence and manifest are in that same private directory: `build-initial.log` (empty successful diagnostics), `build-initial-result.json`, `self-check-initial.log` (SHA256 `6114C5BF9B75A27B4333E3DE38B3CAB08B976B2F01F3158AD8A7127FF7EE0ABB`), `baseline-check.log` (SHA256 `2D82DF14FB345414E164B57177D5A21C426F65C18EB5EF9E010984A92AD2E731`), `checks-initial-result.json` and `manifest.json`. No baseline or produced model files were mutated.
@@ -0,0 +1,85 @@
# Process and scenario canvas interaction decision
Codex-956atf `/root/behavior_audit`, 2026-09-11. Design-only response to [user issue #2](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/2), part of #1. Root assigned this report; no production files, Cargo, native session or Git publication are owned here. The current chooser source is frozen. Codex-9e8r6t owns contextual prerequisites (`df6f595` published in issue #1 comment 242); Codex-ytr83r owns the separately coordinated B4 renderer. Core schema work has another active owner. This proposal does not silently acquire those files or infer another host's current execution state.
## Recommendation and decision boundary
Use one viewport, camera, selection/focus contract and compact contextual palette for architecture, state, process and scenario canvases. Keep a domain-specific scene and semantic gesture adapter behind that shared shell. A process contribution can be positioned freely without changing precedence; a scenario occurrence moves between discrete ordered slots, with its current operand made visible. Both are direct manipulation. Neither requires navigating a Tools shelf, selecting a tiny margin reorder handle, or using a separate list as the primary editor.
The smallest coherent first implementation is a **shared viewport/input shell used by both process and scenario**, with stable canvas bounds, pan/zoom, full-object selection and drag previews, a side palette and point-anchored editing. Reuse the existing guarded command proposals. This is an independently useful slice, but it does not complete #2 until authored process placement and lifeline arrangement survive Save/reopen and the engineering workflows below pass. Replacing button rows alone would leave the main interaction problem intact.
The persistent view contract needs an explicit core-owner decision. Current `ProjectionKind` has no process/scenario owner projection; `Diagram` does already own `NodePlacement` and routes (`oa-core/src/model.rs:1431,1485,1549`). Do not hide new layout in descriptions, abuse unrelated architecture views, or leave accepted visual moves in a desktop-only UUID map. Do not independently select a schema version while schema 8 work is in flight.
## What the code and recorded native work establish
| Surface | Actual implementation | Consequence for this decision |
| --- | --- | --- |
| Architecture | `diagram.rs:39,319,523,966`: Fill viewport, inverse world coordinates, ordered pointer events, whole-node movement, connection/reconnection gestures, pan and wheel zoom. `app/architecture_authoring/palette.rs:167` provides a 36/156 px side palette. | Reuse these interaction patterns. Do not copy all App-specific messages or duplicate the architecture renderer. Current wheel zoom is not evidence of cursor-anchored zoom; make that a stated target. |
| State | `behavior/state_view.rs:47,169` mounts a Fill canvas backed by a real diagram; `state_canvas.rs:729` has focus, world coordinates, full-vertex drag, blank/middle pan, transition endpoints and keyboard camera movement. `state_edit.rs:456` proposes `MoveNode`. | Graphical movement and semantic transition change are separate operations already demonstrated in this codebase. State still has some horizontal controls and a modal details form; those are not requirements to perpetuate. |
| Process/chain | `behavior/topology/view.rs:62,347,618` mounts a model-sized Canvas inside a two-axis Scrollable, Tools/Selection shelves and a lower half-height form. `topology/direct.rs:588,600,615` already supports clicked insertion, connect and reconnect. | It is inaccurate to call the current process image entirely static. The user-facing mismatch is the document/shelf workflow and absent authored placement. |
| Process movement | `topology.rs:892–949` derives 420 px rank columns from links/legacy order. `topology/direct.rs:362–381` refuses movement across those columns and issues `SetSteps`; dragging starts only on the selected handle at `:623`. | Rank is an automatic layout choice, not a semantic prohibition on visual placement. Free visual movement must not be implemented as reordered semantic steps. |
| Scenario | `behavior.rs:1214,1717,1798` projects rows, lanes, slots, fragments and real hit targets. `scenario/workspace.rs:68,206` puts that model-sized scene in a Scrollable, with tool selection in a shelf and Lifeline starting from a separate button. | Preserve real scene semantics and exact clicked-slot capture while changing the host/input contract. Lifeline point placement is a remaining direct-authoring gap. |
| Scenario order/layout | `scenario.rs:935` validates and executes `SetSteps` within the existing parent. `behavior.rs:1230` collects lifelines from children; `model.rs:624–634` stores scenario steps and a represented identity but no lane layout. `model.rs:582` stores fragment operator/branches, not covered lifelines or arbitrary time coordinates. | Vertical order and operand membership are semantic. Horizontal lane position and row spacing are presentation. Drawing a wider fragment must not invent coverage, lifetime or execution semantics. |
These are source observations, not a fresh native acceptance run. The recorded [80cf197 native run](release-scenario-native-20260911.md) passed the four OA clicked-slot creation/history checkpoints, OA reopen, exact caller occurrence/camera return, and SA root Action. It also observed new forms inheriting a prior downward scroll and a captured marker falling below the canvas when the bottom form opened. Root has a separate correction in flight; this design must preserve it until its host is replaced and independently verified. Fit at 21–28% on the minimum window established navigation, not readable editing. The older [process teaching observation](release-teaching-native-20260906.md) found the overview unreadable and less than one column visible at 100%; do not mislabel that older binary as a regression in today's source.
During this design, root freshly re-observed the immutable 80cf197 SA caller at native 1100 px width: 63 objects/11 checks, saved and no draft. Root reports a center canvas of about 502×340 px, its own horizontal/vertical scrollbars, and Tools/zoom/current-tool/Lifeline/property controls taking space before the lane headers; Inspector showed general Project fields. This is root-attributed native confirmation of the interaction framing, not this worker's native run or acceptance of changed source. It supports replacing ordinary layout chrome immediately without tying passive camera changes to a schema migration.
The actual [Capella 7.1 architecture study](release-capella-basic-editing-20260907.md) observed tool → point creation, F2 naming and distinct represented work/allocation. The [scenario study](release-capella-scenario-benchmark-20260910.md) observed Activity point placement with a separate InstanceRole and a dragged LOOP with an operand/start/end records. It did **not** exercise wrapping existing messages, cross-operand moves, reuse or lifecycle drawing. Adopt its direct gestures and identity distinctions; its selection-induced canvas jump is a recorded problem to avoid. No new Capella use or web-derived semantic assumption is claimed here.
## Alternatives considered
| Approach | Concrete operation | Assessment |
| --- | --- | --- |
| Keep the derived document, decorate it as a canvas | Palette replaces Tools; a node drag still changes `steps` only within a computed rank; opening details still splits the document height. | Small change, but the clicked position is not an authored visual position and process movement still depends on arbitrary columns. Useful interim repair only; insufficient response to #2. |
| Universal freeform diagram editor | Every process/scenario box, arrow endpoint and fragment rectangle has unrestricted x/y; order is inferred later or left independent. | Attractive reuse of architecture editing, but creates contradictory scenario order, accidental operand/lifeline reassignment and misleading fragment coverage. Reconciling two competing sources of order would be costly and unsafe. Reject. |
| Shared canvas shell with domain constraints | Same palette, camera, focus, point creation, full-object selection and drag preview; process movement edits view placement, scenario movement snaps to named semantic slots and lane/header layout. | Recommended. Shared input and rendering infrastructure stays small; each domain retains its existing validation and atomic transaction boundary. |
## Interaction contract
**Stable workspace.** Keep the same Fill canvas mounted while arming a tool, selecting an object, typing, showing validation or cancelling. A compact owner/view breadcrumb and camera controls occupy a fixed strip. The side palette is always discoverable, collapsed by default with labels/tooltips and explicitly expandable. Expansion may resize the viewport; other ordinary authoring phases do not. Selection details replace irrelevant general Project fields in the existing Inspector; a small name/reference editor can be anchored at the object. Long structured editing reuses that same Inspector allocation, or an overlaid side drawer if the Inspector is hidden, with bounded scrolling and fixed Apply/Cancel. It must leave a useful part of the canvas visible and must not resize it. There is one draft and one Apply boundary, not a second independent popup copy. Camera input is allowed only outside the editor's bounds; moving the camera updates its anchor or keeps a visible tether, and never changes the captured semantic destination.
**Point creation.** A selected tool previews exactly what a click will mean. In a process, empty space means an unconnected contribution at that world point; the midpoint of a highlighted path means insertion on that exact path. Node hover must not ambiguously mean either “before” or “inside.” In a scenario, a lifeline tool previews a header at the proposed horizontal position. Action, State, Reuse and Fragment preview a named root/operand slot; Action/State on a lane preselect that lane. Message preview shows source, destination and slot. Use the current typed chooser and prerequisite flow in the anchored editor; no placeholder object is accepted before Enter/Apply. Blank-name defaults may be displayed as suggestions without committing them.
**Selection and movement.** Clicking a glyph, its label or a connector selects its precise occurrence/placement. Full node bodies initiate movement after a screen-space threshold; small handles remain for explicit connector endpoints and fragment boundaries, not as the only way to reorder. Process movement changes placement only, leaving all links, guards and semantic `steps` byte-for-byte equivalent. Scenario message/action/state/reuse bodies drag vertically to an explicit insertion line. A fragment header drags the entire fragment subtree as one occurrence. Ordinary drag stays in the current operand; crossing its boundary shows an invalid destination and does not silently reparent. Offer an explicit “Move to operand” operation with an exact destination preview for deliberate cross-operand changes. Lane headers drag horizontally as visual arrangement only; no represented identities, bindings or occurrences change.
**Connections and nesting.** The same click-click and press-drag-release connection grammar applies. Process sequence edges retain their relation ID and guard on reconnection; assembly incidence stays distinct from precedence. A scenario message is an occurrence with message kind/correlation/value/operation fields, not a new sequence link. Reconnecting its endpoint must validate the whole proposed occurrence (and correlated reply constraints) while preserving all unrelated fields; do not silently retarget a call's reply. Fragment tool drag selects a contiguous range of whole siblings within one sequence; wrapping moves those identities into a new operand and replaces their original interval with one fragment in a single transaction. Zero-range click creates the existing empty typed fragment. ALT/PAR provide explicit operand creation/guards at the fragment, and each empty operand has a visible insertion target. A range that slices a child fragment or crosses parents is rejected with visible boundaries. Horizontal drag extent does not assert stored semantic coverage; the frame encloses the required lanes using derived geometry. Lifecycle/activation rendering consumes the separately owned B4 projection unchanged.
**Camera and keyboard.** Use the same transform for draw, hit, overlays and handles: `screen = viewport_origin + pan + zoom * world`. Middle-drag or Space-drag pans; blank left-drag follows the architecture/state default when no creation/range tool owns it. Wheel zoom and explicit +/−/100%/Fit work only over the focused canvas; pointer zoom keeps the world point under the cursor stationary. Fit is an explicit navigation action and does not re-run merely because selection/editor contents change. Keyboard focus is visible; palette and canvas can be reached from the application without pointer input. F2 renames the selected occurrence; Enter edits it; an explicit Open definition command plus Back restores the exact caller use and camera. Arrow keys follow the current state-canvas camera convention; an explicit modified Move command provides nudge/reorder without confusing navigation and model writes. Delete targets the local occurrence/placement, with the existing impact UI for semantic deletion; deleting a process/scenario use never substitutes deletion of its reusable definition. Remove from view is separately labelled where supported. Text fields own their own editing keys.
**Cancel, history and recovery.** Escape cancels the active gesture/editor before changing the selected tool. Press snapshots contain document instance, semantic owner, view, revision, tool/gesture epoch and exact occurrence/path/slot; release validates them again. Mode/owner/revision changes, hidden release or window deactivation cannot commit a stale gesture. A successful semantic or authored-layout drag yields one command/history entry on release; pointer movement and camera movement yield none. Preserve accepted Project/source, raw invalid inputs and captured context across recovery; validate restored destination identities before submission. Camera/selection may be restored as transient presentation, but recovery must retain the application's existing explicit limitation that Undo history is not resumed from a recovery snapshot.
## Persistent presentation and semantic boundaries
Propose owner-bound process and scenario diagram projections through the existing `Diagram`/placement/source machinery, subject to core-owner review. For an imported model with no view, show a derived canvas immediately; materialize the first authored view together with its first layout edit. Merely opening, panning, fitting or inspecting must not dirty source. Use deterministic automatic placement for items lacking authored positions. Explicit Auto arrange is one undoable layout operation; adding a link does not unexpectedly overwrite hand placement.
- Process local contributions use placements of their occurrence IDs. Position/size/routes are graphical; topology links determine engineering precedence. Legacy ordered paths retain their admitted meaning; displaying or moving them must not trigger conversion. The existing `preserved_order` conversion remains part of an explicit semantic editing transaction where required, with the original ordered source restored by Undo.
- Scenario lifeline headers use placements of lifeline IDs, with x/width determining visual arrangement. Do not reorder `Scenario.steps` to reorder lanes. Time rows continue to derive from steps and operand nesting. If manual vertical spacing is later needed, define a view-only minimum-gap contract; arbitrary y is not required for the first complete workflow.
- Reused process previews retain the qualified `OccurrenceEndpoint.references + occurrence` identity. Local use-block layout may be edited; nested definition previews are derived/read-only until opened. Do not key two different uses' nested selections or placements by a bare definition occurrence UUID.
- A new view contract must include shared core validation, identity/reference/deletion handling, source grammar and exact round-trip tests, REST command/schema exposure and historical compatibility. Reuse `NodePlacement` only where its fields have a defined meaning; do not invent a partial desktop-only alternative. Source authors must be able to create the same views/layout without the GUI.
## Minimum complete workflows and falsifiers
Run the same tasks in OA and SA at **1100×700 client** and a wider window, recording actual viewport/input bounds and preserved before/after snapshots. Existing independent semantic oracles remain authoritative; new visual-layout expectations must be fixed before native execution.
| Workflow | Required result and falsifier |
| --- | --- |
| Empty process/chain | Create/open an empty owner, arm Work, click a visible point, choose existing work or invoke its owned “create prerequisite” flow, name, Apply. One local contribution appears at that point; no unrelated allocation or definition replacement. Cancel leaves every accepted source byte unchanged. A forced trip through a Tools shelf or an offscreen form fails the workflow. |
| Process structure | Place a second work, control and exchange/reuse, connect two paths, name a guard, reconnect a chosen endpoint and insert on one of two separately guarded links. Verify exact relation IDs, endpoint/guard retention and no accidental link picked by proximity alone. Move a node across former rank columns; only the view placement changes. Repeat on legacy ordered input, preserving original source on Undo. |
| Empty scenario | Create each supported viewpoint, place two lifelines by canvas points, select represented objects with context, reorder headers and Save/reopen. Both IDs and visual order persist; scenario steps and representations do not change merely from lane drag. New source creation uses the incoming contextual prerequisite adapter, not a second implementation. |
| Message and action | Draw source-to-target by both gesture styles; create Action/State at a clicked root and nested-operand slot; edit values/operation and a call/reply where admitted. Compare full body fields and exact before-anchor, parent, sender/receiver, correlation and values. Invalid endpoint edits retain the original occurrence and the editable raw attempt. |
| Nested control | Create empty ALT/PAR/LOOP/OPT, add an operand at its boundary, insert into an empty nested operand, wrap a contiguous existing range, move a whole fragment and explicitly transfer an occurrence to a chosen operand. Verify parent lists, branches, moved IDs and all untouched siblings. Cross-parent drag and partial-subtree wrapping must visibly refuse before any command. Width alone must not change modeled lane participation. |
| Reuse navigation | A→B→C and Back twice, then open the second distinct A→B use. Return selects the exact initiating occurrence, restores the owner/view and camera once, and allows subsequent camera input without snapping back. Renamed uses remain valid; deleted/retargeted uses and document reload invalidate old returns safely. |
| Camera and access | At 50/100/200% and nonzero pan, glyph/label/endpoint hits, click-create, drag and popup anchors agree. Include negative world coordinates, edges, resize and coalesced pointer events. At minimum size, palette, name/first field, required values and Apply/Cancel are reachable with visible focus; Tab reveals offscreen fields. Fit is not used as a substitute for legible 100% editing. |
| Cancel/history/recovery | Escape at every phase; release after pan/tool/owner/revision change; valid and invalid source/property/behavior drafts; delete/Undo/Redo; pending recovery. Accepted model plus complete SourceBundle must match saved pre/Undo/Redo snapshots exactly, including identities and comments. Layout edits have their own exact one-step expectation; camera/selection/search have none. Restored raw invalid values must remain raw and unapplied. |
| Prerequisites and long lists | With no compatible definition, invoke the already-owned staged prerequisite flow in captured canvas context. Create work/exchange/participant only after an explicit choice; no automatic allocation or reassignment. For 1000 repeated names, query and selection show kind/layer/containment/alias and bounded results, preserve the clicked destination, and do not apply while searching. Historical UUID text stays outside live-reference choosing. |
Mounted tests must feed actual ordered key/pointer batches through the root Iced view, not directly set an internal drag state. Independent App tests assert complete commands, source and raw drafts. Native Windows and supported Linux work remain separate gates, including real-font wrapping, keyboard ownership, touchpad input, popup geometry and usable latency. Screen-reader semantics of custom Canvas glyphs are not established by pointer/keyboard tests: retain a synchronized, keyboard-accessible outline as an alternate representation and measure platform accessibility before claiming it. The outline is optional access, not a required step for ordinary canvas editing.
## Proposed implementation partitions
1. **Shared shell experiment for both behavior scenes (desktop lease).** New small `canvas_workspace` camera/focus/gesture-context primitives and palette host; adapt process `topology/view.rs,direct.rs` and scenario `workspace.rs,behavior.rs` to Fill viewport/world coordinates. Use existing Draft and Command adapters. Capture absolute point/slot/path in typed events, present their one existing editor as an anchored surface, retain App pending guards and exact caller-return context. Replace lower split-form and Tools-shelf dependence only after both scenes pass mounted origin/geometry/stale-event and minimum native workflows. Do not require an immediate rewrite of architecture/state or build a generic graph semantic framework.
2. **Authored view/placement contract (core + source + REST owner lease).** Ratify owner-bound projections and placement rules above with schema owner, migrate/import absent views, validate/delete/reference them and add independent exact source fixtures. Then process creation/movement and scenario lane placement use those shared commands. This is necessary before claiming persistent direct canvas editing complete; no fake desktop persistence in partition 1.
3. **Domain manipulation completion (separate bounded desktop/core command lease).** Full-object process drag with view placement; scenario slot drag, message endpoint edit, range-wrap and explicit operand transfer. `oa-core/src/session.rs:67,81,82` already exposes `Reparent`, `SetSteps` and `MoveNode`; reuse those commands where their final batch validates, adding a core helper only to centralize genuine shared semantics. Preserve explicit before/after lists and original IDs in wrapping/transfer tests; do not infer their correctness from the final drawing. Keep B4 rendering and incoming prerequisite drafts intact. Acceptance is the workflow matrix, not the number of tools moved into a palette.
Root should ratify the interaction and persistent-view boundary with the independent critic before assigning these production leases. This report is ready for that decision; it records no implementation or new execution pass.
+635
View File
@@ -0,0 +1,635 @@
//! Independent contextual-source native oracle. No desktop producer is imported.
//! self-check BASE | check STAGE BASE ACTUAL [FIRST_LIFELINE [FIRST_CHAIN [FIRST_MACHINE]]]
//! same EXPECTED ACTUAL [UNDO_FROM]
use oa_core::{source_project::*, *};
use std::{
collections::{BTreeMap, BTreeSet},
fs,
path::Path,
};
const ACTIVITY: &str = "Assess incident work";
const LIFELINE: &str = "Assessment lifeline";
const FUNCTION: &str = "Classify request";
const CONTRIBUTION: &str = "Classify this request";
const ACTOR: &str = "Field rescue team";
const MACHINE: &str = "Field team operating modes";
const VIEW: &str = "Field team operating modes · states";
const SCENARIO: u128 = 0xd1510000000040008000000000000002;
const CHAIN: u128 = 0xd1510000000040008000000000000003;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
enum Stage {
Baseline,
Lifeline,
Chain,
Machine,
}
impl Stage {
fn parse(text: &str) -> Self {
match text {
"baseline" => Self::Baseline,
"lifeline" => Self::Lifeline,
"chain" => Self::Chain,
"machine" => Self::Machine,
_ => panic!("Stage must be baseline, lifeline, chain or machine"),
}
}
fn additions(self) -> usize {
match self {
Self::Baseline => 0,
Self::Lifeline => 2,
Self::Chain => 4,
Self::Machine => 6,
}
}
}
type DiskSources = BTreeMap<String, Vec<u8>>;
fn source_paths(root: &Path, here: &Path, paths: &mut DiskSources) {
for entry in fs::read_dir(here).unwrap() {
let entry = entry.unwrap();
let kind = entry.file_type().unwrap();
assert!(
!kind.is_symlink(),
"Checkpoint files must be ordinary files"
);
if kind.is_dir() {
source_paths(root, &entry.path(), paths);
} else if entry.path().extension().is_some_and(|ext| ext == "arcadia") {
assert!(kind.is_file());
paths.insert(
entry
.path()
.strip_prefix(root)
.unwrap()
.to_str()
.unwrap()
.replace('\\', "/"),
fs::read(entry.path()).unwrap(),
);
}
}
}
fn load_checkpoint(entry: &Path) -> (CompiledSourceProject, DiskSources) {
let files = source_files::load_project(entry, ProjectLimits::default()).unwrap();
assert!(
!files
.diagnostics
.iter()
.any(|d| d.diagnostic.severity == validation::Severity::Error),
"Loader errors: {:?}",
files.diagnostics
);
let raw = files.reachable_bundle();
let compiled = compile_bundle(&raw, ProjectLimits::default()).unwrap();
assert_eq!(
compiled.bundle, raw,
"No normalization, migration or missing identity is allowed in saved output"
);
execute_batch(&compiled.decoded.project, &[]).unwrap();
let mut disk = BTreeMap::new();
let root = entry.parent().unwrap();
source_paths(root, root, &mut disk);
(compiled, disk)
}
fn predicted_disk(reachable: &SourceBundle, undo_from: Option<&DiskSources>) -> DiskSources {
let mut files = undo_from.cloned().unwrap_or_default();
files.extend(
reachable
.files
.iter()
.map(|(path, text)| (path.clone(), text.as_bytes().to_vec())),
);
files
}
fn load(entry: &Path) -> CompiledSourceProject {
let (compiled, files) = load_checkpoint(entry);
assert_eq!(
files,
predicted_disk(&compiled.bundle, None),
"Ordinary checkpoint has unexpected detached source files; Undo needs an explicit origin"
);
compiled
}
fn baseline(entry: &Path) -> CompiledSourceProject {
let base = load(entry);
assert_eq!(
base.bundle,
SourceBundle {
entry: "project.arcadia".into(),
files: BTreeMap::from([
(
"project.arcadia".into(),
include_str!("fixtures/staged-source-authoring/base/project.arcadia").into()
),
(
"notes.arcadia".into(),
include_str!("fixtures/staged-source-authoring/base/notes.arcadia").into()
),
])
},
"Baseline bytes differ from the pre-observation fixture embedded in this executable"
);
let p = &base.decoded.project;
assert_eq!(p.elements.len(), 2);
assert!(p.relations.is_empty());
assert!(p.diagrams.is_empty());
assert_eq!(
p.elements[&Id::from_u128(SCENARIO)].name,
"Operational assessment"
);
assert_eq!(p.elements[&Id::from_u128(CHAIN)].name, "System assessment");
base
}
struct Roles<'a> {
actual: &'a Project,
baseline: &'a Project,
used: BTreeSet<Id>,
}
impl Roles<'_> {
fn fresh(&mut self, ids: Vec<Id>, role: &str) -> Id {
assert_eq!(ids.len(), 1, "Exactly one literal role required: {role}");
let id = ids[0];
assert_eq!(id.get_version_num(), 4, "Fresh role must be UUIDv4: {role}");
assert!(!self.baseline.contains(id));
assert!(self.used.insert(id));
id
}
fn element(&mut self, kind: Kind, layer: Layer, name: &str) -> Id {
self.fresh(
self.actual
.elements
.values()
.filter(|e| e.kind() == kind && e.layer() == layer && e.name == name)
.map(|e| e.id)
.collect(),
name,
)
}
fn view(&mut self, machine: Id) -> Id {
self.fresh(
self.actual
.diagrams
.values()
.filter(|d| {
d.name == VIEW
&& d.layer == Layer::Sa
&& d.projection == ProjectionKind::StateMachine { machine }
})
.map(|d| d.id)
.collect(),
VIEW,
)
}
fn placement(&mut self, view: Id, machine: Id) -> Id {
self.fresh(
self.actual.diagrams[&view]
.nodes
.values()
.filter(|n| n.element == machine)
.map(|n| n.id)
.collect(),
"first view machine placement",
)
}
}
fn element(
project: &Project,
id: Id,
name: &str,
parent: Option<Id>,
body: ElementBody,
) -> Element {
Element {
id,
alias: project.fresh_alias(name, "element", id),
name: name.into(),
description: String::new(),
parent,
body,
}
}
fn stage_commands(before: &Project, stage: Stage, roles: &mut Roles<'_>) -> Vec<Command> {
match stage {
Stage::Baseline => vec![],
Stage::Lifeline => {
let source = roles.element(Kind::Activity, Layer::Oa, ACTIVITY);
let line = roles.element(Kind::Lifeline, Layer::Oa, LIFELINE);
vec![
Command::CreateElement(element(
before,
source,
ACTIVITY,
None,
ElementBody::Oa(OaBody::Activity),
)),
Command::CreateElement(element(
before,
line,
LIFELINE,
Some(Id::from_u128(SCENARIO)),
ElementBody::shared_in(
Layer::Oa,
SharedBody::Lifeline {
represented: Some(source),
},
),
)),
]
}
Stage::Chain => {
let source = roles.element(Kind::Function, Layer::Sa, FUNCTION);
let step = roles.element(Kind::Occurrence, Layer::Sa, CONTRIBUTION);
vec![
Command::CreateElement(element(
before,
source,
FUNCTION,
None,
ElementBody::Sa(SaBody::Function),
)),
Command::CreateElement(element(
before,
step,
CONTRIBUTION,
Some(Id::from_u128(CHAIN)),
ElementBody::shared_in(
Layer::Sa,
SharedBody::Occurrence {
body: OccurrenceBody::Action {
action: Some(source),
lifeline: None,
},
precondition: String::new(),
postcondition: String::new(),
},
),
)),
Command::SetSteps(Id::from_u128(CHAIN), vec![step]),
]
}
Stage::Machine => {
let actor = roles.element(Kind::SystemActor, Layer::Sa, ACTOR);
let machine = roles.element(Kind::StateMachine, Layer::Sa, MACHINE);
let diagram = roles.view(machine);
let placement = roles.placement(diagram, machine);
let owner = element(before, actor, ACTOR, None, ElementBody::Sa(SaBody::Actor));
let record = element(
before,
machine,
MACHINE,
Some(actor),
ElementBody::shared_in(Layer::Sa, SharedBody::StateMachine),
);
let p = execute_batch(
before,
&[
Command::CreateElement(owner.clone()),
Command::CreateElement(record.clone()),
],
)
.unwrap();
let view = Diagram {
id: diagram,
alias: p.fresh_alias(VIEW, "state_view", diagram),
name: VIEW.into(),
description: String::new(),
layer: Layer::Sa,
projection: ProjectionKind::StateMachine { machine },
routes: vec![],
view_options: Default::default(),
nodes: BTreeMap::from([(
placement,
NodePlacement {
id: placement,
alias: p.fresh_alias(
&format!("{MACHINE} placement"),
"placement",
placement,
),
element: machine,
position: Position { x: 60., y: 60. },
size: Size {
width: 336.,
height: 104.,
},
collapsed: false,
port_anchors: vec![],
},
)]),
};
vec![
Command::CreateElement(owner),
Command::CreateElement(record),
Command::CreateDiagram(view),
Command::Layout(diagram),
]
}
}
}
fn predict(
base: &CompiledSourceProject,
stage: Stage,
carriers: &[&Project],
) -> CompiledSourceProject {
let mut result = base.clone();
let mut used = BTreeSet::new();
for (index, current) in [Stage::Lifeline, Stage::Chain, Stage::Machine]
.into_iter()
.filter(|s| *s <= stage)
.enumerate()
{
let mut roles = Roles {
actual: carriers[index],
baseline: &base.decoded.project,
used,
};
let command = Command::Batch(stage_commands(&result.decoded.project, current, &mut roles));
used = roles.used;
let plan = prepare_command(&result.decoded.project, &command).unwrap();
result = update_bundle_with_identity_map(
&result,
&plan.project,
&plan.identity_map,
ProjectLimits::default(),
)
.unwrap();
}
assert_eq!(
result.decoded.project.elements.len(),
base.decoded.project.elements.len() + stage.additions()
);
assert_eq!(
result.decoded.project.diagrams.len(),
usize::from(stage == Stage::Machine)
);
assert_eq!(
result.decoded.project.relations,
base.decoded.project.relations
);
assert_eq!(result.decoded.project.traces, base.decoded.project.traces);
assert_eq!(
result.decoded.project.artifact_links,
base.decoded.project.artifact_links
);
result
}
fn equal(a: &CompiledSourceProject, b: &CompiledSourceProject) -> bool {
a.decoded.project == b.decoded.project && a.bundle == b.bundle
}
fn assert_equal(a: &CompiledSourceProject, b: &CompiledSourceProject) {
assert_eq!(
a.decoded.project, b.decoded.project,
"Complete Project differs"
);
assert_eq!(a.bundle, b.bundle, "Complete exact SourceBundle differs");
}
fn test_id(n: u128) -> Id {
Id::from_u128(0xd2520000000040008000000000000000 + n)
}
fn false_carrier(base: &Project) -> Project {
let mut p = base.clone();
for (n, kind, layer, name) in [
(1, Kind::Activity, Layer::Oa, ACTIVITY),
(2, Kind::Lifeline, Layer::Oa, LIFELINE),
(3, Kind::Function, Layer::Sa, FUNCTION),
(4, Kind::Occurrence, Layer::Sa, CONTRIBUTION),
(5, Kind::SystemActor, Layer::Sa, ACTOR),
(6, Kind::StateMachine, Layer::Sa, MACHINE),
] {
p.elements.insert(
test_id(n),
Element {
id: test_id(n),
alias: format!("false_alias_{n}"),
name: name.into(),
description: "Never copy this observed metadata".into(),
parent: None,
body: ElementBody::new(kind, layer).unwrap(),
},
);
}
p.diagrams.insert(
test_id(7),
Diagram {
id: test_id(7),
alias: "false_view_alias".into(),
name: VIEW.into(),
description: "Never copy this".into(),
layer: Layer::Sa,
projection: ProjectionKind::StateMachine {
machine: test_id(6),
},
routes: vec![],
view_options: Default::default(),
nodes: BTreeMap::from([(
test_id(8),
NodePlacement {
id: test_id(8),
alias: "false_placement_alias".into(),
element: test_id(6),
position: Position { x: -123., y: 987. },
size: Size {
width: 77.,
height: 33.,
},
collapsed: true,
port_anchors: vec![],
},
)]),
},
);
p
}
fn self_check(base: &CompiledSourceProject) {
let carrier = false_carrier(&base.decoded.project);
let mut previous = base.clone();
for stage in [Stage::Lifeline, Stage::Chain, Stage::Machine] {
let expected = predict(base, stage, &[&carrier; 3]);
assert_equal(
&expected,
&predict(base, stage, &[&expected.decoded.project; 3]),
);
assert_equal(
&expected,
&compile_bundle(&expected.bundle, ProjectLimits::default()).unwrap(),
);
let mut roles = Roles {
actual: &carrier,
baseline: &base.decoded.project,
used: BTreeSet::new(),
};
let command = Command::Batch(stage_commands(&previous.decoded.project, stage, &mut roles));
let mut session = Session::try_new(previous.decoded.project.clone()).unwrap();
session.execute(command.clone()).unwrap();
assert_eq!(session.project(), &expected.decoded.project);
assert!(
session.execute(command).is_err(),
"Exact repeated creation command must not duplicate identities"
);
assert_eq!(session.project(), &expected.decoded.project);
session.undo();
assert_eq!(session.project(), &previous.decoded.project);
session.redo();
assert_eq!(session.project(), &expected.decoded.project);
let mut changed = expected.clone();
changed
.decoded
.project
.elements
.get_mut(&test_id(1))
.unwrap()
.description = "unrequested".into();
assert!(!equal(&expected, &changed));
let mut changed_source = expected.bundle.clone();
changed_source
.files
.get_mut("notes.arcadia")
.unwrap()
.push_str("// drift\n");
let changed = compile_bundle(&changed_source, ProjectLimits::default()).unwrap();
assert_eq!(changed.decoded.project, expected.decoded.project);
assert!(!equal(&expected, &changed));
let committed_disk = predicted_disk(&expected.bundle, None);
let undo_disk = predicted_disk(&previous.bundle, Some(&committed_disk));
assert!(undo_disk.len() >= previous.bundle.files.len());
let mut unknown = undo_disk.clone();
unknown.insert("unknown.arcadia".into(), b"// unknown".to_vec());
assert_ne!(unknown, undo_disk);
let detached: Vec<_> = undo_disk
.keys()
.filter(|p| !previous.bundle.files.contains_key(*p))
.cloned()
.collect();
assert!(
!detached.is_empty(),
"Each selected source operation should add a module"
);
let mut changed = undo_disk.clone();
changed.get_mut(&detached[0]).unwrap().push(b' ');
assert_ne!(changed, undo_disk);
let mut missing = undo_disk.clone();
missing.remove(&detached[0]);
assert_ne!(missing, undo_disk);
println!(
"PASS {stage:?}: complete model/source fixed point, false-carrier metadata ignored, repeated-command refusal, one Undo/Redo, comment drift and explicit retained-file falsifiers; {} reachable files",
expected.bundle.files.len()
);
previous = expected;
}
let expected = predict(base, Stage::Machine, &[&carrier; 3]);
let mut wrong = expected.clone();
wrong
.decoded
.project
.elements
.get_mut(&test_id(2))
.unwrap()
.body = ElementBody::shared_in(Layer::Oa, SharedBody::Lifeline { represented: None });
assert!(!equal(&expected, &wrong));
let mut wrong = expected.clone();
wrong
.decoded
.project
.elements
.get_mut(&test_id(6))
.unwrap()
.parent = None;
assert!(!equal(&expected, &wrong));
let mut wrong = expected.clone();
wrong
.decoded
.project
.diagrams
.get_mut(&test_id(7))
.unwrap()
.nodes
.get_mut(&test_id(8))
.unwrap()
.position
.x += 1.;
assert!(!equal(&expected, &wrong));
println!(
"PASS wrong source binding, machine owner and view geometry rejected; no native acceptance claimed"
);
let mut changed_prior_role = carrier.clone();
let mut changed_activity = changed_prior_role.elements.remove(&test_id(1)).unwrap();
changed_activity.id = test_id(91);
changed_prior_role
.elements
.insert(test_id(91), changed_activity);
let pinned = predict(base, Stage::Chain, &[&carrier, &changed_prior_role]);
assert_equal(&pinned, &predict(base, Stage::Chain, &[&carrier; 3]));
assert!(!equal(
&pinned,
&predict(base, Stage::Chain, &[&changed_prior_role; 3])
));
println!("PASS earlier-stage role identities remain pinned independently of a later carrier");
}
fn main() {
assert_eq!(SCHEMA_VERSION, 7);
let args: Vec<_> = std::env::args().collect();
match args.get(1).map(String::as_str) {
Some("self-check") => {
assert_eq!(args.len(), 3);
self_check(&baseline(Path::new(&args[2])));
}
Some("check") => {
assert!(
args.len() >= 5,
"check STAGE BASE ACTUAL [FIRST_LIFELINE [FIRST_CHAIN [FIRST_MACHINE]]]"
);
let stage = Stage::parse(&args[2]);
let stages: Vec<_> = [Stage::Lifeline, Stage::Chain, Stage::Machine]
.into_iter()
.filter(|s| *s <= stage)
.collect();
assert_eq!(
args.len(),
5 + stages.len(),
"Supply every stage's original first-commit role carrier separately"
);
let base = baseline(Path::new(&args[3]));
let actual = load(Path::new(&args[4]));
let carriers: Vec<_> = args[5..].iter().map(|path| load(Path::new(path))).collect();
let projects: Vec<_> = carriers.iter().map(|c| &c.decoded.project).collect();
for (index, carrier_stage) in stages.into_iter().enumerate() {
assert_equal(&predict(&base, carrier_stage, &projects), &carriers[index]);
}
let expected = predict(&base, stage, &projects);
assert_equal(&expected, &actual);
println!(
"PASS {stage:?}: complete Project + {} exact source/disk files, original role UUIDs",
actual.bundle.files.len()
);
}
Some("same") => {
assert!(
matches!(args.len(), 4 | 5),
"same EXPECTED ACTUAL [UNDO_FROM]"
);
let expected = load(Path::new(&args[2]));
let (actual, disk) = load_checkpoint(Path::new(&args[3]));
assert_equal(&expected, &actual);
let origin = args
.get(4)
.map(|path| predicted_disk(&load(Path::new(path)).bundle, None));
assert_eq!(
disk,
predicted_disk(&expected.bundle, origin.as_ref()),
"Only exact first-commit retained files are permitted after Undo"
);
println!(
"PASS exact model/identity/{} reachable sources; {} exact disk files ({} proven retained)",
actual.bundle.files.len(),
disk.len(),
disk.len() - actual.bundle.files.len()
);
}
_ => panic!(
"self-check BASE | check STAGE BASE ACTUAL [FIRST_LIFELINE [FIRST_CHAIN [FIRST_MACHINE]]] | same EXPECTED ACTUAL [UNDO_FROM]"
),
}
}