Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
555f2b9d6d | ||
|
|
563772dc62 | ||
|
|
17d56c06f7 | ||
|
|
36eff7225a | ||
|
|
b4c8fd7102 | ||
|
|
d8bdbd9e48 | ||
|
|
1e9f0c2a97 | ||
|
|
b6b76efc0d | ||
|
|
01bb52a76d |
@@ -1,13 +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.
|
||||
- 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.
|
||||
|
||||
+57
-569
@@ -1,589 +1,50 @@
|
||||
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
|
||||
|
||||
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. Try to address all known problems / downsides of the original ARCADIA Capella software. When writing code and making software architecture decisions focus on long term maintainability, extensibility while making the project friendly for active development
|
||||
### 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
|
||||
|
||||
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 1-st release version of the project with fully feature complete Operation Analysis + System Architecture, according to this application specification.
|
||||
And the remaining ARCADIA layers will be added in future releases.
|
||||
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.
|
||||
|
||||
@@ -596,24 +57,50 @@ You should implement most of the ARCADIA semantics, while adressing original ARC
|
||||
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 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
|
||||
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
|
||||
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 the 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
|
||||
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
|
||||
|
||||
### Semantic model / metamodel
|
||||
|
||||
Requirements:
|
||||
- Textual notation.
|
||||
- Graphical notation.
|
||||
- Constraints and calculations.
|
||||
- Quantities and units.
|
||||
- State and behavior modeling.
|
||||
- Reusable model libraries.
|
||||
- Strong, standardized specification.
|
||||
- Strong, standardized API.
|
||||
|
||||
#### 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.
|
||||
- System engineers should be able to easily develop system models (both big and small) using textual notation only.
|
||||
|
||||
### UI
|
||||
|
||||
The focus should be on reducing system engineer's cognitive load when creating / working with a system model. Regardless of the model size (big or small).
|
||||
|
||||
Long, homogeneous lists in the UI that require user to scroll far or search for a desired option for too long - is a bad design.
|
||||
|
||||
## 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
|
||||
@@ -621,3 +108,4 @@ Right now, the behavior & scenarios UI is hard to use. It contains static / uned
|
||||
---
|
||||
|
||||
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 -->
|
||||
|
||||
@@ -1101,11 +1101,13 @@ impl App {
|
||||
// still validates document, scenario and revision before accepting it.
|
||||
if matches!(
|
||||
&message,
|
||||
Message::ScenarioCanvas(crate::behavior::scenario::CanvasEvent::Scrolled(..))
|
||||
| Message::TopologyCanvas(
|
||||
crate::behavior::topology::CanvasEvent::Scrolled(..)
|
||||
| crate::behavior::topology::CanvasEvent::Viewport(..)
|
||||
)
|
||||
Message::ScenarioCanvas(
|
||||
crate::behavior::scenario::CanvasEvent::Scrolled(..)
|
||||
| crate::behavior::scenario::CanvasEvent::Restored(..)
|
||||
) | Message::TopologyCanvas(
|
||||
crate::behavior::topology::CanvasEvent::Scrolled(..)
|
||||
| crate::behavior::topology::CanvasEvent::Viewport(..)
|
||||
)
|
||||
) {
|
||||
return self.handle(message);
|
||||
}
|
||||
@@ -1363,6 +1365,18 @@ impl App {
|
||||
Message::ScenarioCanvas(event) => {
|
||||
use crate::behavior::scenario::{self, CanvasEvent};
|
||||
let context = event.context();
|
||||
if let CanvasEvent::Restored(_, token, area, offset) = event {
|
||||
if self.scenario_context().is_some_and(|current| {
|
||||
current.document == context.document && current.scenario == context.scenario
|
||||
}) && self
|
||||
.behavior
|
||||
.scenario
|
||||
.restored(context, token, area, offset)
|
||||
{
|
||||
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
|
||||
}
|
||||
return Task::none();
|
||||
}
|
||||
if let CanvasEvent::Scrolled(_, area, offset) = event {
|
||||
// Inner and outer scroll notifications may come from the
|
||||
// same frame. Accept both across the first epoch bump,
|
||||
@@ -1381,6 +1395,45 @@ impl App {
|
||||
return Task::none();
|
||||
}
|
||||
match event {
|
||||
CanvasEvent::OpenUse(_, occurrence) => {
|
||||
if self.behavior.has_pending() || self.draft_dirty() || self.source_dirty()
|
||||
{
|
||||
self.status = "Apply or cancel pending edits before opening a referenced scenario.".into();
|
||||
return Task::none();
|
||||
}
|
||||
let mut state = std::mem::take(&mut self.behavior.scenario);
|
||||
let target = state.open_use(self.project(), context, occurrence);
|
||||
self.behavior.scenario = state;
|
||||
match target {
|
||||
Ok(target) => {
|
||||
self.select(target);
|
||||
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
|
||||
}
|
||||
Err(error) => self.status = error,
|
||||
}
|
||||
}
|
||||
CanvasEvent::Back(_) => {
|
||||
if self.behavior.has_pending() || self.draft_dirty() || self.source_dirty()
|
||||
{
|
||||
self.status =
|
||||
"Apply or cancel pending edits before returning to the caller."
|
||||
.into();
|
||||
return Task::none();
|
||||
}
|
||||
let mut state = std::mem::take(&mut self.behavior.scenario);
|
||||
let caller = state.back(self.project(), context);
|
||||
self.behavior.scenario = state;
|
||||
if let Some(caller) = caller {
|
||||
self.select(caller.occurrence);
|
||||
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
|
||||
if let Some(context) = self.scenario_context() {
|
||||
self.behavior.scenario.restore_caller(context, caller);
|
||||
}
|
||||
} else {
|
||||
self.status =
|
||||
"The calling scenario reference is no longer available.".into();
|
||||
}
|
||||
}
|
||||
CanvasEvent::Select(_, id) => {
|
||||
if !self.behavior.has_pending()
|
||||
&& crate::behavior::selected_behavior(self.project(), id)
|
||||
@@ -1406,6 +1459,29 @@ impl App {
|
||||
return Task::none();
|
||||
}
|
||||
match mutation {
|
||||
CanvasEvent::BeginOccurrence {
|
||||
tool,
|
||||
slot,
|
||||
lifeline,
|
||||
..
|
||||
} => {
|
||||
let mut state = std::mem::take(&mut self.behavior.scenario);
|
||||
let result = state.begin_occurrence(
|
||||
self.project(),
|
||||
context,
|
||||
tool,
|
||||
slot,
|
||||
lifeline,
|
||||
);
|
||||
self.behavior.scenario = state;
|
||||
match result {
|
||||
Ok(()) => {
|
||||
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
|
||||
self.status = "Complete the selected behavior at the highlighted sequence position.".into();
|
||||
}
|
||||
Err(error) => self.status = error,
|
||||
}
|
||||
}
|
||||
CanvasEvent::Begin {
|
||||
slot,
|
||||
sender,
|
||||
|
||||
@@ -343,7 +343,7 @@ impl App {
|
||||
Event::Name(value) => {
|
||||
if value.len() <= 16384
|
||||
&& let Some(s) = &mut self.architecture
|
||||
&& let (Intent::Create { name, .. } | Intent::Rename { name, .. }) =
|
||||
&& let Intent::Create { name, .. } | Intent::Rename { name, .. } =
|
||||
&mut s.saved.intent
|
||||
{
|
||||
*name = value;
|
||||
@@ -422,7 +422,10 @@ impl App {
|
||||
}
|
||||
}
|
||||
Event::Click(context, point) => {
|
||||
if self.route_context() != Some(context) {
|
||||
if self.route_context() != Some(context)
|
||||
|| !point.x.is_finite()
|
||||
|| !point.y.is_finite()
|
||||
{
|
||||
return Task::none();
|
||||
}
|
||||
if self.architecture.as_ref().is_some_and(|s| s.saved.placing) {
|
||||
@@ -460,6 +463,10 @@ impl App {
|
||||
self.status =
|
||||
"The diagram changed. The name is retained; cancel and restart this placement."
|
||||
.into();
|
||||
if let Some(s) = &mut self.architecture {
|
||||
s.saved.placing = false;
|
||||
s.error = Some(self.status.clone());
|
||||
}
|
||||
return;
|
||||
}
|
||||
let diagram = s.saved.diagram;
|
||||
|
||||
@@ -4,7 +4,7 @@ use iced::{
|
||||
Point, Rectangle, Size, Theme, mouse,
|
||||
widget::{
|
||||
canvas::{self, Frame, Geometry, Path, Stroke},
|
||||
container, tooltip,
|
||||
column, container, tooltip,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -279,6 +279,166 @@ fn architecture_cancelled_gesture_cannot_commit_a_later_named_draft() {
|
||||
assert!(app.architecture.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn architecture_f2_rename_preserves_identity_placement_and_atomic_history() {
|
||||
use iced::{keyboard, mouse, widget::canvas::Program};
|
||||
let mut app = setup(Layer::Oa);
|
||||
send(
|
||||
&mut app,
|
||||
Event::Start(Kind::Entity, Some(Position { x: 200., y: 120. })),
|
||||
);
|
||||
send(&mut app, Event::Name("Response team".into()));
|
||||
send(&mut app, Event::Submit);
|
||||
let before = app.document.bundle().clone();
|
||||
let element = app.selected;
|
||||
let node = app.selected_node;
|
||||
let diagrams = app.project().diagrams.clone();
|
||||
let camera = (app.zoom, app.pan);
|
||||
let canvas = crate::diagram::DiagramCanvas {
|
||||
project: app.project(),
|
||||
view_index: None,
|
||||
diagram: app.active_diagram.unwrap(),
|
||||
selected: element,
|
||||
zoom: app.zoom,
|
||||
pan: app.pan,
|
||||
context: app.route_context().unwrap(),
|
||||
selected_bend: None,
|
||||
editable: true,
|
||||
placing: false,
|
||||
connection: None,
|
||||
};
|
||||
let bounds = iced::Rectangle::with_size(iced::Size::new(800., 600.));
|
||||
let cursor = mouse::Cursor::Available(iced::Point::new(500., 400.));
|
||||
let mut state = crate::diagram::State::default();
|
||||
let key = iced::widget::canvas::Event::Keyboard(keyboard::Event::KeyPressed {
|
||||
key: keyboard::Key::Named(keyboard::key::Named::F2),
|
||||
modified_key: keyboard::Key::Named(keyboard::key::Named::F2),
|
||||
physical_key: keyboard::key::Physical::Code(keyboard::key::Code::F2),
|
||||
location: keyboard::Location::Standard,
|
||||
modifiers: keyboard::Modifiers::empty(),
|
||||
text: None,
|
||||
repeat: false,
|
||||
});
|
||||
assert!(canvas.update(&mut state, &key, bounds, cursor).is_none());
|
||||
let _ = canvas.update(
|
||||
&mut state,
|
||||
&iced::widget::canvas::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
|
||||
bounds,
|
||||
cursor,
|
||||
);
|
||||
let message = canvas
|
||||
.update(&mut state, &key, bounds, cursor)
|
||||
.expect("focused F2 opens the selected semantic name")
|
||||
.into_inner()
|
||||
.0
|
||||
.unwrap();
|
||||
let _ = app.update(message);
|
||||
assert!(app.architecture.as_ref().unwrap().inline());
|
||||
send(&mut app, Event::Name(" Emergency team 雨 ".into()));
|
||||
assert_eq!(app.document.bundle(), &before);
|
||||
send(&mut app, Event::Submit);
|
||||
assert!(app.architecture.is_none());
|
||||
assert_eq!((app.selected, app.selected_node), (element, node));
|
||||
assert_eq!(
|
||||
app.project().elements[&element].name,
|
||||
" Emergency team 雨 "
|
||||
);
|
||||
assert_eq!(app.project().diagrams, diagrams);
|
||||
assert_eq!((app.zoom, app.pan), camera);
|
||||
let accepted = app.document.bundle().clone();
|
||||
let _ = app.update(Message::Undo);
|
||||
assert_eq!(app.document.bundle(), &before);
|
||||
let _ = app.update(Message::Redo);
|
||||
assert_eq!(app.document.bundle(), &accepted);
|
||||
let context = app.route_context().unwrap();
|
||||
send(
|
||||
&mut app,
|
||||
Event::Rename(context, element, Position { x: 200., y: 120. }),
|
||||
);
|
||||
send(&mut app, Event::Name("Cancelled name".into()));
|
||||
let _ = app.update(Message::Escape);
|
||||
assert!(app.architecture.is_none());
|
||||
assert_eq!(app.document.bundle(), &accepted);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn architecture_contextual_allocation_creates_work_relation_and_node_in_one_undo() {
|
||||
for (layer, performer_kind, work_kind) in [
|
||||
(Layer::Oa, Kind::Entity, Kind::Activity),
|
||||
(Layer::Sa, Kind::System, Kind::Function),
|
||||
] {
|
||||
let mut app = setup(layer);
|
||||
send(
|
||||
&mut app,
|
||||
Event::Start(performer_kind, Some(Position { x: 20., y: 20. })),
|
||||
);
|
||||
send(&mut app, Event::Name("Control team".into()));
|
||||
send(&mut app, Event::Submit);
|
||||
let performer = app.selected;
|
||||
let before = app.document.bundle().clone();
|
||||
send(
|
||||
&mut app,
|
||||
Event::Allocate(work_kind, performer, Some(Position { x: 55., y: 90. })),
|
||||
);
|
||||
send(&mut app, Event::Name("Assess pressure".into()));
|
||||
assert_eq!(app.document.bundle(), &before);
|
||||
send(&mut app, Event::Submit);
|
||||
assert!(app.architecture.is_none(), "{}", app.status);
|
||||
let work = app.selected;
|
||||
assert_eq!(app.project().elements[&work].parent, None);
|
||||
assert!(
|
||||
app.project()
|
||||
.relations
|
||||
.values()
|
||||
.any(|r| r.kind() == RelationKind::Allocation
|
||||
&& r.source() == performer
|
||||
&& r.target() == work)
|
||||
);
|
||||
let node = &app.project().diagrams[&app.active_diagram.unwrap()].nodes
|
||||
[&app.selected_node.unwrap()];
|
||||
assert_eq!(node.position, Position { x: 55., y: 90. });
|
||||
let accepted = app.document.bundle().clone();
|
||||
let _ = app.update(Message::Undo);
|
||||
assert_eq!(app.document.bundle(), &before);
|
||||
let _ = app.update(Message::Redo);
|
||||
assert_eq!(app.document.bundle(), &accepted);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn architecture_rejected_submission_keeps_inline_name_visible_and_invalid_click_is_ignored() {
|
||||
let mut app = setup(Layer::Oa);
|
||||
send(&mut app, Event::Start(Kind::Entity, None));
|
||||
let context = app.route_context().unwrap();
|
||||
send(
|
||||
&mut app,
|
||||
Event::Click(
|
||||
context,
|
||||
Position {
|
||||
x: f32::NAN,
|
||||
y: 30.,
|
||||
},
|
||||
),
|
||||
);
|
||||
assert!(app.architecture.as_ref().unwrap().unnamed_tool());
|
||||
send(&mut app, Event::Click(context, Position { x: 40., y: 30. }));
|
||||
send(&mut app, Event::Name("Retained team".into()));
|
||||
// A replaced document must not accept a queued draft submission.
|
||||
app.canvas_document = Id::new_v4();
|
||||
let before = app.document.bundle().clone();
|
||||
send(&mut app, Event::Submit);
|
||||
let draft = app.architecture.as_ref().unwrap();
|
||||
assert!(draft.inline());
|
||||
assert!(draft.error.is_some());
|
||||
assert!(
|
||||
matches!(&draft.saved.intent, architecture_authoring::Intent::Create { name, .. } if name == "Retained team")
|
||||
);
|
||||
assert_eq!(app.document.bundle(), &before);
|
||||
let _ = app.update(Message::Escape);
|
||||
assert!(app.architecture.is_none());
|
||||
assert_eq!(app.document.bundle(), &before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_transition_explicit_overview_focus_wins_over_an_already_populated_architecture() {
|
||||
use crate::workflow::{Event as W, Workflow};
|
||||
|
||||
@@ -32,6 +32,7 @@ mod scenario_scroll {
|
||||
pub(super) fn observe<'a>(
|
||||
context: scenario::Context,
|
||||
area: scenario::ScrollArea,
|
||||
restore: Option<scenario::ScrollRestore>,
|
||||
content: impl Into<Element<'a, Message>>,
|
||||
) -> Element<'a, Message> {
|
||||
Element::new(Observer {
|
||||
@@ -39,6 +40,7 @@ mod scenario_scroll {
|
||||
context,
|
||||
area,
|
||||
process: false,
|
||||
restore,
|
||||
})
|
||||
}
|
||||
pub(super) fn observe_topology<'a>(
|
||||
@@ -56,6 +58,7 @@ mod scenario_scroll {
|
||||
},
|
||||
area,
|
||||
process: true,
|
||||
restore: None,
|
||||
})
|
||||
}
|
||||
struct Observer<'a, R = Renderer> {
|
||||
@@ -63,11 +66,13 @@ mod scenario_scroll {
|
||||
context: scenario::Context,
|
||||
area: scenario::ScrollArea,
|
||||
process: bool,
|
||||
restore: Option<scenario::ScrollRestore>,
|
||||
}
|
||||
type Report = Option<(bool, Id, Id, Vector)>;
|
||||
struct ObservedScroll {
|
||||
identity: (Id, Id),
|
||||
report: Report,
|
||||
restored: Option<Id>,
|
||||
}
|
||||
#[derive(Default)]
|
||||
struct Translation(Option<Vector>);
|
||||
@@ -84,6 +89,28 @@ mod scenario_scroll {
|
||||
self.0 = Some(translation);
|
||||
}
|
||||
}
|
||||
struct RestoreTranslation {
|
||||
offset: Vector,
|
||||
applied: bool,
|
||||
}
|
||||
impl widget::Operation for RestoreTranslation {
|
||||
// Restore only this wrapper's root Scrollable, never its nested canvas.
|
||||
fn traverse(&mut self, _: &mut dyn FnMut(&mut dyn widget::Operation)) {}
|
||||
fn scrollable(
|
||||
&mut self,
|
||||
_: Option<&widget::Id>,
|
||||
_: Rectangle,
|
||||
_: Rectangle,
|
||||
_: Vector,
|
||||
state: &mut dyn widget::operation::Scrollable,
|
||||
) {
|
||||
state.scroll_to(widget::operation::scrollable::AbsoluteOffset {
|
||||
x: Some(self.offset.x),
|
||||
y: Some(self.offset.y),
|
||||
});
|
||||
self.applied = true;
|
||||
}
|
||||
}
|
||||
impl<R: renderer::Renderer> Observer<'_, R> {
|
||||
fn report(
|
||||
&mut self,
|
||||
@@ -92,6 +119,25 @@ mod scenario_scroll {
|
||||
renderer: &R,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) {
|
||||
let restore = self.restore.filter(|request| {
|
||||
!self.process
|
||||
&& (request.document, request.owner)
|
||||
== (self.context.document, self.context.scenario)
|
||||
&& tree.state.downcast_ref::<ObservedScroll>().restored != Some(request.token)
|
||||
});
|
||||
let receipt = restore.and_then(|request| {
|
||||
let mut operation = RestoreTranslation {
|
||||
offset: request.offset,
|
||||
applied: false,
|
||||
};
|
||||
self.content.as_widget_mut().operate(
|
||||
&mut tree.children[0],
|
||||
layout,
|
||||
renderer,
|
||||
&mut operation,
|
||||
);
|
||||
operation.applied.then_some(request.token)
|
||||
});
|
||||
let mut offset = Translation::default();
|
||||
self.content.as_widget_mut().operate(
|
||||
&mut tree.children[0],
|
||||
@@ -106,7 +152,19 @@ mod scenario_scroll {
|
||||
self.context.scenario,
|
||||
offset,
|
||||
));
|
||||
let previous = &mut tree.state.downcast_mut::<ObservedScroll>().report;
|
||||
let state = tree.state.downcast_mut::<ObservedScroll>();
|
||||
if let Some(token) = receipt {
|
||||
state.restored = Some(token);
|
||||
state.report = next;
|
||||
shell.publish(Message::ScenarioCanvas(scenario::CanvasEvent::Restored(
|
||||
self.context,
|
||||
token,
|
||||
self.area,
|
||||
offset,
|
||||
)));
|
||||
return;
|
||||
}
|
||||
let previous = &mut state.report;
|
||||
if *previous != next {
|
||||
*previous = next;
|
||||
shell.publish(if self.process {
|
||||
@@ -139,6 +197,7 @@ mod scenario_scroll {
|
||||
widget::tree::State::new(ObservedScroll {
|
||||
identity: (self.context.document, self.context.scenario),
|
||||
report: None,
|
||||
restored: None,
|
||||
})
|
||||
}
|
||||
fn children(&self) -> Vec<widget::Tree> {
|
||||
@@ -274,6 +333,7 @@ mod scenario_scroll {
|
||||
context,
|
||||
area: scenario::ScrollArea::Canvas,
|
||||
process: false,
|
||||
restore: None,
|
||||
content: scrollable(iced::widget::Space::new().width(200.).height(height))
|
||||
.id("observer-test")
|
||||
.width(200.)
|
||||
@@ -320,6 +380,154 @@ mod scenario_scroll {
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
fn actual_offset(view: &mut Observer<'_, ()>, tree: &mut widget::Tree) -> Vector {
|
||||
let node = view.layout(
|
||||
tree,
|
||||
&(),
|
||||
&layout::Limits::new(Size::ZERO, Size::new(800., 700.)),
|
||||
);
|
||||
let mut operation = Translation::default();
|
||||
view.operate(tree, Layout::new(&node), &(), &mut operation);
|
||||
operation.0.unwrap()
|
||||
}
|
||||
#[test]
|
||||
fn direct_scenario_caller_restores_mounted_scroll_once_with_clamping_and_app_receipt() {
|
||||
use scenario::{
|
||||
CanvasEvent, ScrollArea,
|
||||
direct_tests::{navigation_event, navigation_fixture},
|
||||
};
|
||||
let redraw = || {
|
||||
iced::Event::Window(iced::window::Event::RedrawRequested(
|
||||
std::time::Instant::now(),
|
||||
))
|
||||
};
|
||||
for area in [ScrollArea::Canvas, ScrollArea::Page] {
|
||||
for returned_height in [600., 250.] {
|
||||
let (project, [a, _, _], [use_id, _, _]) = navigation_fixture();
|
||||
let mut app = App::with_project(project.clone());
|
||||
let _ = app.update(Message::Select(a));
|
||||
let _ = app.update(Message::Page(crate::app::Page::Behavior));
|
||||
let source = app.document.bundle().clone();
|
||||
let mut view = observer(600., app.scenario_context().unwrap());
|
||||
view.area = area;
|
||||
let mut tree = widget::Tree::new(&view as &dyn Widget<Message, Theme, ()>);
|
||||
for message in update(&mut view, &mut tree, redraw()).0 {
|
||||
let _ = app.update(message);
|
||||
}
|
||||
for message in update(
|
||||
&mut view,
|
||||
&mut tree,
|
||||
iced::Event::Mouse(mouse::Event::WheelScrolled {
|
||||
delta: mouse::ScrollDelta::Pixels { x: 0., y: -180. },
|
||||
}),
|
||||
)
|
||||
.0
|
||||
{
|
||||
let _ = app.update(message);
|
||||
}
|
||||
assert_eq!(actual_offset(&mut view, &mut tree), Vector::new(0., 180.));
|
||||
navigation_event(&mut app, |ctx| CanvasEvent::OpenUse(ctx, use_id));
|
||||
view = observer(600., app.scenario_context().unwrap());
|
||||
view.area = area;
|
||||
view.diff(&mut tree);
|
||||
for message in update(&mut view, &mut tree, redraw()).0 {
|
||||
let _ = app.update(message);
|
||||
}
|
||||
assert_eq!(actual_offset(&mut view, &mut tree), Vector::ZERO);
|
||||
navigation_event(&mut app, CanvasEvent::Back);
|
||||
assert_eq!(app.selected, use_id);
|
||||
let context = app.scenario_context().unwrap();
|
||||
let request = app.behavior.scenario.scroll_restore(context, area).unwrap();
|
||||
view = observer(returned_height, context);
|
||||
view.area = area;
|
||||
view.restore = Some(request);
|
||||
view.diff(&mut tree);
|
||||
let expected = Vector::new(0., (returned_height - 200.).min(180.));
|
||||
let messages = update(&mut view, &mut tree, redraw()).0;
|
||||
assert!(
|
||||
matches!(messages.as_slice(), [Message::ScenarioCanvas(CanvasEvent::Restored(_, _, _, actual))] if *actual == expected)
|
||||
);
|
||||
assert_eq!(actual_offset(&mut view, &mut tree), expected);
|
||||
for message in messages {
|
||||
let _ = app.update(message);
|
||||
}
|
||||
assert!(
|
||||
app.behavior
|
||||
.scenario
|
||||
.scroll_restore(app.scenario_context().unwrap(), area)
|
||||
.is_none()
|
||||
);
|
||||
// A delayed receipt leaves the same request in the view; later input must not snap back.
|
||||
for message in update(
|
||||
&mut view,
|
||||
&mut tree,
|
||||
iced::Event::Mouse(mouse::Event::WheelScrolled {
|
||||
delta: mouse::ScrollDelta::Pixels { x: 0., y: 20. },
|
||||
}),
|
||||
)
|
||||
.0
|
||||
{
|
||||
let _ = app.update(message);
|
||||
}
|
||||
let moved = expected - Vector::new(0., 20.);
|
||||
assert_eq!(actual_offset(&mut view, &mut tree), moved);
|
||||
view.context = app.scenario_context().unwrap();
|
||||
view.diff(&mut tree);
|
||||
assert!(update(&mut view, &mut tree, redraw()).0.is_empty());
|
||||
assert_eq!(actual_offset(&mut view, &mut tree), moved);
|
||||
navigation_event(&mut app, |ctx| {
|
||||
CanvasEvent::Restored(ctx, request.token, area, Vector::new(0., 999.))
|
||||
});
|
||||
let late_context = app.scenario_context().unwrap();
|
||||
let _ = app.update(Message::Page(crate::app::Page::Diagram));
|
||||
let epoch = app.canvas_epoch;
|
||||
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Restored(
|
||||
late_context,
|
||||
request.token,
|
||||
area,
|
||||
Vector::ZERO,
|
||||
)));
|
||||
assert_eq!(app.canvas_epoch, epoch);
|
||||
assert_eq!(app.project(), &project);
|
||||
assert_eq!(app.document.bundle(), &source);
|
||||
assert!(!app.document.can_undo());
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn direct_scenario_restore_cannot_scroll_another_document_owner_or_process_view() {
|
||||
for mismatch in 0..3 {
|
||||
let context = scenario::direct_tests::context(Id::new_v4());
|
||||
let mut request = scenario::ScrollRestore {
|
||||
document: context.document,
|
||||
owner: context.scenario,
|
||||
token: Id::new_v4(),
|
||||
offset: Vector::new(0., 180.),
|
||||
};
|
||||
match mismatch {
|
||||
0 => request.document = Id::new_v4(),
|
||||
1 => request.owner = Id::new_v4(),
|
||||
_ => {}
|
||||
}
|
||||
let mut view = observer(600., context);
|
||||
view.process = mismatch == 2;
|
||||
view.restore = Some(request);
|
||||
let mut tree = widget::Tree::new(&view as &dyn Widget<Message, Theme, ()>);
|
||||
let messages = update(
|
||||
&mut view,
|
||||
&mut tree,
|
||||
iced::Event::Window(iced::window::Event::RedrawRequested(
|
||||
std::time::Instant::now(),
|
||||
)),
|
||||
)
|
||||
.0;
|
||||
assert_eq!(actual_offset(&mut view, &mut tree), Vector::ZERO);
|
||||
assert!(!messages.iter().any(|m| matches!(
|
||||
m,
|
||||
Message::ScenarioCanvas(scenario::CanvasEvent::Restored(..))
|
||||
)));
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn independent_process_owner_change_resets_actual_scroll_offsets() {
|
||||
let context = scenario::direct_tests::context(Id::new_v4());
|
||||
@@ -711,8 +919,7 @@ pub fn view(app: &App) -> Element<'_, Message> {
|
||||
return out.push(label("Start with a Process, Scenario or State machine using Create element. Add occurrences or regions inside it to describe behavior.")).padding(24).into();
|
||||
};
|
||||
let e = &p.elements[&selected.id];
|
||||
let compact_process = matches!(e.kind(), Kind::Process | Kind::FunctionalChain)
|
||||
&& (uses_explicit_topology(p, e.id) || e.steps().is_none_or(Vec::is_empty));
|
||||
let compact_process = matches!(e.kind(), Kind::Process | Kind::FunctionalChain);
|
||||
if e.kind() == Kind::StateMachine {
|
||||
out = column![text("States & modes").size(20)].spacing(8);
|
||||
} else if compact_process {
|
||||
@@ -841,6 +1048,7 @@ struct Scene {
|
||||
scale: f32,
|
||||
scroll_offset: iced::Vector,
|
||||
interaction: Option<(scenario::Context, scenario::CanvasTool, bool)>,
|
||||
pending_placement: Option<(scenario::Slot, Option<Id>)>,
|
||||
lanes: BTreeMap<Id, f32>,
|
||||
slots: Vec<SequenceSlot>,
|
||||
rows: BTreeMap<Id, (Id, f32)>,
|
||||
@@ -863,6 +1071,11 @@ struct SequenceInput {
|
||||
}
|
||||
#[derive(Clone, Copy)]
|
||||
enum SequencePress {
|
||||
Occurrence {
|
||||
tool: scenario::Tool,
|
||||
slot: scenario::Slot,
|
||||
lifeline: Option<Id>,
|
||||
},
|
||||
Message {
|
||||
source: Id,
|
||||
slot: scenario::Slot,
|
||||
@@ -893,6 +1106,7 @@ impl Scene {
|
||||
size: Size::new(700., 200.),
|
||||
selected,
|
||||
interaction: None,
|
||||
pending_placement: None,
|
||||
lanes: BTreeMap::new(),
|
||||
slots: vec![],
|
||||
rows: BTreeMap::new(),
|
||||
@@ -1518,6 +1732,7 @@ impl canvas::Program<Message> for Scene {
|
||||
| SequencePress::Reorder { start, moved, .. } => {
|
||||
*moved |= position.distance(*start) >= 3.
|
||||
}
|
||||
SequencePress::Occurrence { .. } => {}
|
||||
}
|
||||
}
|
||||
mouse::Cursor::Available(*position + self.scroll_offset)
|
||||
@@ -1576,6 +1791,17 @@ impl canvas::Program<Message> for Scene {
|
||||
});
|
||||
return Some(Action::request_redraw().and_capture());
|
||||
}
|
||||
if let CanvasTool::Add(tool) = tool {
|
||||
if !tool.canvas_occurrence() {
|
||||
return None;
|
||||
}
|
||||
state.press = Some(SequencePress::Occurrence {
|
||||
tool,
|
||||
slot: self.slot_at(point, None)?.slot,
|
||||
lifeline: tool.uses_lifeline().then(|| self.lane_at(point)).flatten(),
|
||||
});
|
||||
return Some(Action::request_redraw().and_capture());
|
||||
}
|
||||
if self.reorder_handle().is_some_and(|r| r.contains(point)) {
|
||||
state.press = Some(SequencePress::Reorder {
|
||||
occurrence: self.selected,
|
||||
@@ -1597,6 +1823,23 @@ impl canvas::Program<Message> for Scene {
|
||||
let press = state.press.take()?;
|
||||
let context = context?;
|
||||
match press {
|
||||
SequencePress::Occurrence {
|
||||
tool: captured,
|
||||
slot,
|
||||
lifeline,
|
||||
} if tool == CanvasTool::Add(captured) && world.is_some() => {
|
||||
return Some(
|
||||
Action::publish(Message::ScenarioCanvas(
|
||||
CanvasEvent::BeginOccurrence {
|
||||
context,
|
||||
tool: captured,
|
||||
slot,
|
||||
lifeline,
|
||||
},
|
||||
))
|
||||
.and_capture(),
|
||||
);
|
||||
}
|
||||
SequencePress::Message { source, slot, .. }
|
||||
if tool == CanvasTool::Message(scenario::MessageStyle::Timer)
|
||||
&& world.is_some() =>
|
||||
@@ -1783,8 +2026,8 @@ impl canvas::Program<Message> for Scene {
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
if let Some((context, tool, true)) = self.interaction {
|
||||
let active = state.context == Some(context) && state.tool == tool;
|
||||
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));
|
||||
@@ -1796,7 +2039,8 @@ impl canvas::Program<Message> for Scene {
|
||||
None
|
||||
}
|
||||
});
|
||||
if tool == scenario::CanvasTool::Select
|
||||
if editable
|
||||
&& tool == scenario::CanvasTool::Select
|
||||
&& let Some(handle) = self.reorder_handle()
|
||||
{
|
||||
f.fill_rectangle(handle.position(), handle.size(), style::TEAL);
|
||||
@@ -1808,9 +2052,25 @@ impl canvas::Program<Message> for Scene {
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
let slot = if let Some((_, slot)) = source {
|
||||
let placement = self.pending_placement.or_else(|| {
|
||||
active.then_some(state.press).flatten().and_then(|press| {
|
||||
if let SequencePress::Occurrence { slot, lifeline, .. } = press {
|
||||
Some((slot, lifeline))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
});
|
||||
let slot = if let Some((slot, _)) = placement {
|
||||
self.slots.iter().find(|s| s.slot == slot)
|
||||
} else if matches!(tool, scenario::CanvasTool::Message(_)) || reorder.is_some() {
|
||||
} else if let Some((_, slot)) = source {
|
||||
self.slots.iter().find(|s| s.slot == slot)
|
||||
} else if editable
|
||||
&& (matches!(
|
||||
tool,
|
||||
scenario::CanvasTool::Message(_) | scenario::CanvasTool::Add(_)
|
||||
) || reorder.is_some())
|
||||
{
|
||||
point.and_then(|p| self.slot_at(p, reorder))
|
||||
} else {
|
||||
None
|
||||
@@ -1829,12 +2089,29 @@ impl canvas::Program<Message> for Scene {
|
||||
Color::WHITE,
|
||||
);
|
||||
f.fill_text(Text {
|
||||
content: short(&format!("Insert: {}", slot.label), 88),
|
||||
content: short(
|
||||
&format!(
|
||||
"{}: {}",
|
||||
if self.pending_placement.is_some() {
|
||||
"Pending"
|
||||
} else {
|
||||
"Insert"
|
||||
},
|
||||
slot.label
|
||||
),
|
||||
88,
|
||||
),
|
||||
position: Point::new(38., slot.y - 19.),
|
||||
size: 11.into(),
|
||||
color: style::TEAL,
|
||||
..Default::default()
|
||||
});
|
||||
if let Some(x) = placement
|
||||
.and_then(|(_, lifeline)| lifeline)
|
||||
.and_then(|id| self.lanes.get(&id))
|
||||
{
|
||||
f.fill(&Path::circle(Point::new(*x, slot.y), 5.), style::TEAL);
|
||||
}
|
||||
if let Some((source, _)) = source
|
||||
&& let Some(x) = self.lanes.get(&source)
|
||||
&& let Some(point) = point
|
||||
@@ -2246,6 +2523,110 @@ mod direct_canvas_tests {
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn direct_clicked_occurrence_capture_cancels_on_view_and_context_changes() {
|
||||
for tool in [
|
||||
scenario::Tool::Action,
|
||||
scenario::Tool::Observation,
|
||||
scenario::Tool::Reuse,
|
||||
scenario::Tool::Fragment,
|
||||
] {
|
||||
for cancellation in 0..9 {
|
||||
let (f, mut scene) = scene();
|
||||
scene.interaction.as_mut().unwrap().1 = CanvasTool::Add(tool);
|
||||
let mut state = SequenceInput::default();
|
||||
let mut bounds = Rectangle::new(Point::ORIGIN, scene.size);
|
||||
let point = Point::new(scene.lanes[&f.sender], scene.slots[0].y);
|
||||
send(
|
||||
&scene,
|
||||
&mut state,
|
||||
mouse::Event::CursorMoved { position: point },
|
||||
point,
|
||||
bounds,
|
||||
);
|
||||
assert!(
|
||||
send(
|
||||
&scene,
|
||||
&mut state,
|
||||
mouse::Event::ButtonPressed(mouse::Button::Left),
|
||||
point,
|
||||
bounds
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
assert!(matches!(
|
||||
state.press,
|
||||
Some(SequencePress::Occurrence { .. })
|
||||
));
|
||||
let cancellation_event = match cancellation {
|
||||
0 => {
|
||||
scene.interaction.as_mut().unwrap().0.epoch += 1;
|
||||
None
|
||||
}
|
||||
1 => {
|
||||
scene.interaction.as_mut().unwrap().1 = CanvasTool::Select;
|
||||
None
|
||||
}
|
||||
2 => {
|
||||
scene.interaction.as_mut().unwrap().2 = false;
|
||||
None
|
||||
}
|
||||
3 => Some(canvas::Event::Mouse(mouse::Event::WheelScrolled {
|
||||
delta: mouse::ScrollDelta::Lines { x: 0., y: -1. },
|
||||
})),
|
||||
4 => Some(canvas::Event::Window(iced::window::Event::Resized(
|
||||
Size::new(1100., 700.),
|
||||
))),
|
||||
5 => {
|
||||
bounds.y -= 30.;
|
||||
None
|
||||
}
|
||||
6 => Some(canvas::Event::Window(iced::window::Event::Unfocused)),
|
||||
7 => Some(canvas::Event::Keyboard(iced::keyboard::Event::KeyPressed {
|
||||
key: iced::keyboard::Key::Named(iced::keyboard::key::Named::Escape),
|
||||
modified_key: iced::keyboard::Key::Named(
|
||||
iced::keyboard::key::Named::Escape,
|
||||
),
|
||||
physical_key: iced::keyboard::key::Physical::Code(
|
||||
iced::keyboard::key::Code::Escape,
|
||||
),
|
||||
location: iced::keyboard::Location::Standard,
|
||||
modifiers: iced::keyboard::Modifiers::empty(),
|
||||
text: None,
|
||||
repeat: false,
|
||||
})),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(event) = cancellation_event {
|
||||
let action = canvas::Program::update(
|
||||
&scene,
|
||||
&mut state,
|
||||
&event,
|
||||
bounds,
|
||||
mouse::Cursor::Available(point),
|
||||
);
|
||||
assert!(action.and_then(|action| action.into_inner().0).is_none());
|
||||
}
|
||||
let cursor = if cancellation == 8 {
|
||||
mouse::Cursor::Unavailable
|
||||
} else {
|
||||
mouse::Cursor::Available(point)
|
||||
};
|
||||
let released = canvas::Program::update(
|
||||
&scene,
|
||||
&mut state,
|
||||
&canvas::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)),
|
||||
bounds,
|
||||
cursor,
|
||||
);
|
||||
assert!(
|
||||
released.and_then(|action| action.into_inner().0).is_none(),
|
||||
"{tool:?} cancellation {cancellation}"
|
||||
);
|
||||
assert!(state.press.is_none());
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn direct_selected_handle_reorders_and_hits_keep_semantic_identity() {
|
||||
let (f, mut scene) = scene();
|
||||
scene.interaction.as_mut().unwrap().1 = CanvasTool::Select;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -836,6 +836,12 @@ struct GraphEdge {
|
||||
incidence: bool,
|
||||
layer: EdgeLayer,
|
||||
}
|
||||
// Ordered exchange adjacency is a projection, never a persisted relation ID.
|
||||
struct OrderedPath {
|
||||
source: Id,
|
||||
target: Id,
|
||||
points: Vec<Point>,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum EdgeLayer {
|
||||
Direct,
|
||||
@@ -863,6 +869,8 @@ struct Graph<'a> {
|
||||
scroll_offset: iced::Vector,
|
||||
nodes: Vec<GraphNode>,
|
||||
edges: Vec<GraphEdge>,
|
||||
ordered_paths: Vec<OrderedPath>,
|
||||
ordered: bool,
|
||||
size: Size,
|
||||
selected: Id,
|
||||
scale: f32,
|
||||
@@ -884,11 +892,17 @@ impl<'a> Graph<'a> {
|
||||
let context_index = oa_core::topology_context::TopologyIndex::new(p);
|
||||
let choices = contributions(p, owner);
|
||||
let links = sequence_links(p, owner);
|
||||
let order = legacy_order(p, owner);
|
||||
let adjacency: Vec<_> = links
|
||||
.iter()
|
||||
.map(|r| (r.source(), r.target()))
|
||||
.chain(order.windows(2).map(|pair| (pair[0], pair[1])))
|
||||
.collect();
|
||||
let ids: BTreeSet<_> = choices.iter().map(|c| c.id).collect();
|
||||
let mut incoming: BTreeMap<_, usize> = ids.iter().map(|id| (*id, 0)).collect();
|
||||
let mut ranks: BTreeMap<_, usize> = ids.iter().map(|id| (*id, 0)).collect();
|
||||
for r in &links {
|
||||
*incoming.entry(r.target()).or_default() += 1;
|
||||
for &(_, target) in &adjacency {
|
||||
*incoming.entry(target).or_default() += 1;
|
||||
}
|
||||
let mut queue: Vec<_> = choices
|
||||
.iter()
|
||||
@@ -903,8 +917,7 @@ impl<'a> Graph<'a> {
|
||||
if !done.insert(id) {
|
||||
continue;
|
||||
}
|
||||
for r in links.iter().filter(|r| r.source() == id) {
|
||||
let next = r.target();
|
||||
for &(_, next) in adjacency.iter().filter(|(source, _)| *source == id) {
|
||||
ranks.insert(next, ranks[&next].max(ranks[&id] + 1));
|
||||
let count = incoming.get_mut(&next).expect("Graph endpoint");
|
||||
*count = count.saturating_sub(1);
|
||||
@@ -1031,6 +1044,29 @@ impl<'a> Graph<'a> {
|
||||
size.height = size.height.max(node.rect.y + node.rect.height + 42.);
|
||||
nodes.push(node);
|
||||
}
|
||||
let ordered_paths = order
|
||||
.windows(2)
|
||||
.map(|pair| {
|
||||
let a = nodes
|
||||
.iter()
|
||||
.find(|n| n.id == pair[0])
|
||||
.expect("ordered source")
|
||||
.rect;
|
||||
let b = nodes
|
||||
.iter()
|
||||
.find(|n| n.id == pair[1])
|
||||
.expect("ordered target")
|
||||
.rect;
|
||||
OrderedPath {
|
||||
source: pair[0],
|
||||
target: pair[1],
|
||||
points: vec![
|
||||
Point::new(a.x + a.width, a.center_y()),
|
||||
Point::new(b.x, b.center_y()),
|
||||
],
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let mut edges = vec![];
|
||||
let mut routed_pairs = BTreeSet::new();
|
||||
for (i, r) in links.into_iter().enumerate() {
|
||||
@@ -1165,6 +1201,8 @@ impl<'a> Graph<'a> {
|
||||
scroll_offset: iced::Vector::ZERO,
|
||||
nodes,
|
||||
edges,
|
||||
ordered_paths,
|
||||
ordered: !order.is_empty(),
|
||||
size,
|
||||
selected,
|
||||
scale: 1.,
|
||||
@@ -1264,6 +1302,18 @@ impl canvas::Program<Message> for Graph<'_> {
|
||||
for pass in PAINT_PASSES {
|
||||
match pass {
|
||||
PaintPass::DirectEdges | PaintPass::ContextualEdges => {
|
||||
if pass == PaintPass::DirectEdges {
|
||||
for path in &self.ordered_paths {
|
||||
let selected =
|
||||
path.source == self.selected || path.target == self.selected;
|
||||
draw_arrow(
|
||||
&mut f,
|
||||
&path.points,
|
||||
if selected { style::TEAL } else { style::MUTED },
|
||||
if selected { 2.5 } else { 1.5 },
|
||||
);
|
||||
}
|
||||
}
|
||||
for e in self.edges.iter().filter(|edge| {
|
||||
edge.layer
|
||||
== if pass == PaintPass::DirectEdges {
|
||||
@@ -1465,6 +1515,11 @@ impl canvas::Program<Message> for Graph<'_> {
|
||||
if cursor.position_in(bounds).is_some_and(|p| {
|
||||
let p = Point::new(p.x / self.scale, p.y / self.scale);
|
||||
self.nodes.iter().any(|n| n.rect.contains(p))
|
||||
|| self.ordered_paths.iter().any(|path| {
|
||||
path.points.windows(2).any(|pair| {
|
||||
crate::diagram::distance_to_segment(p, pair[0], pair[1]) < 8. / self.scale
|
||||
})
|
||||
})
|
||||
|| self.edges.iter().any(|e| {
|
||||
e.label.contains(p)
|
||||
|| e.points
|
||||
|
||||
@@ -238,9 +238,7 @@ impl State {
|
||||
) {
|
||||
return Err("Choose a contribution tool.".into());
|
||||
}
|
||||
if !legacy_order(p, context.owner).is_empty() {
|
||||
return Err("Convert the ordered path to explicit links before drawing.".into());
|
||||
}
|
||||
let order = legacy_order(p, context.owner);
|
||||
let link = insertion
|
||||
.link
|
||||
.map(|id| {
|
||||
@@ -268,6 +266,13 @@ impl State {
|
||||
if let Some(r) = link {
|
||||
d.before = Some(r.source());
|
||||
d.after = Some(r.target());
|
||||
} else if !order.is_empty() {
|
||||
let index = insertion
|
||||
.before
|
||||
.and_then(|id| order.iter().position(|v| *v == id))
|
||||
.unwrap_or(order.len());
|
||||
d.before = index.checked_sub(1).map(|i| order[i]);
|
||||
d.after = order.get(index).copied();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -416,6 +421,13 @@ impl Input {
|
||||
}
|
||||
}
|
||||
impl Graph<'_> {
|
||||
fn ordered_path_at(&self, point: Point) -> Option<&OrderedPath> {
|
||||
self.ordered_paths.iter().find(|path| {
|
||||
path.points.windows(2).any(|pair| {
|
||||
crate::diagram::distance_to_segment(point, pair[0], pair[1]) < 8. / self.scale
|
||||
})
|
||||
})
|
||||
}
|
||||
fn node_at(&self, point: Point) -> Option<Id> {
|
||||
self.nodes
|
||||
.iter()
|
||||
@@ -579,7 +591,9 @@ impl Graph<'_> {
|
||||
link: self
|
||||
.edge_at(point)
|
||||
.filter(|_| self.node_at(point).is_none()),
|
||||
before: self.node_at(point),
|
||||
before: self.node_at(point).or_else(|| {
|
||||
self.ordered_path_at(point).map(|path| path.target)
|
||||
}),
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -807,6 +821,13 @@ impl Graph<'_> {
|
||||
draw_arrow(f, &e.points, style::TEAL, 3.);
|
||||
}
|
||||
"Insert on this path · preserve its guard"
|
||||
} else if let Some(path) = self.ordered_path_at(point) {
|
||||
draw_arrow(f, &path.points, style::TEAL, 3.);
|
||||
"Insert on ordered path · preserve its order"
|
||||
} else if self.ordered && self.node_at(point).is_some() {
|
||||
"Insert before this contribution · preserve path"
|
||||
} else if self.ordered {
|
||||
"Continue the ordered path"
|
||||
} else if self.node_at(point).is_some() {
|
||||
"Add before this contribution · unconnected"
|
||||
} else {
|
||||
|
||||
@@ -970,6 +970,359 @@ fn direct_process_append_preserves_existing_order_and_rejects_incompatible_excha
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_interactive_owner_view(p: &Project, owner: Id) {
|
||||
use iced::advanced::widget::Tree;
|
||||
fn contains(tree: &Tree, tag: iced::advanced::widget::tree::Tag) -> bool {
|
||||
tree.tag == tag || tree.children.iter().any(|child| contains(child, tag))
|
||||
}
|
||||
let mut app = App::with_project(p.clone());
|
||||
app.layer = p.elements[&owner].layer();
|
||||
app.page = crate::app::Page::Behavior;
|
||||
app.selected = owner;
|
||||
app.behavior.active_owner = Some(owner);
|
||||
let graph: Element<'_, Message> = canvas::Canvas::new(Graph::new(p, owner, owner)).into();
|
||||
let tag = Tree::new(graph.as_widget()).tag;
|
||||
let view = view::view(&app, owner, Column::new());
|
||||
assert!(
|
||||
contains(&Tree::new(view.as_widget()), tag),
|
||||
"every path uses the interactive graph"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_process_three_ordered_exchanges_preserve_outer_paths_on_insertion() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let (mut f, next) = empty_exchange_fixture(layer);
|
||||
f.p = execute_batch(
|
||||
&f.p,
|
||||
&[Command::Connect(
|
||||
f.p.relations[&f.exchange].kind(),
|
||||
"Return for reassessment".into(),
|
||||
f.work[2],
|
||||
f.work[0],
|
||||
)],
|
||||
)
|
||||
.unwrap();
|
||||
let last =
|
||||
f.p.relations
|
||||
.values()
|
||||
.find(|r| r.name == "Return for reassessment")
|
||||
.unwrap()
|
||||
.id;
|
||||
for exchange in [f.exchange, next, last] {
|
||||
let node = occurrence(
|
||||
&f.p,
|
||||
f.owner,
|
||||
"Ordered contribution",
|
||||
OccurrenceBody::Exchange {
|
||||
operation: None,
|
||||
exchange: Some(exchange),
|
||||
sender: None,
|
||||
receiver: None,
|
||||
message_kind: MessageKind::default(),
|
||||
reply_to: None,
|
||||
values: vec![],
|
||||
},
|
||||
);
|
||||
f.p = execute_batch(&f.p, &[Command::CreateElement(node)]).unwrap();
|
||||
}
|
||||
for index in [0, 2] {
|
||||
let mut doc = Document::from_project(f.p.clone(), ProjectLimits::default()).unwrap();
|
||||
let before = doc.bundle().clone();
|
||||
let old_order = legacy_order(doc.project(), f.owner);
|
||||
let graph = Graph::new(doc.project(), f.owner, f.owner);
|
||||
assert_eq!(graph.ordered_paths.len(), 2);
|
||||
assert!(graph.edges.is_empty());
|
||||
let mut state = State::default();
|
||||
state
|
||||
.begin(
|
||||
doc.project(),
|
||||
context(f.owner),
|
||||
Tool::Work,
|
||||
ControlKind::Choice,
|
||||
Insertion {
|
||||
link: None,
|
||||
before: Some(old_order[index]),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
state
|
||||
.update(
|
||||
doc.project(),
|
||||
Event::Target(Choice {
|
||||
id: f.work[index],
|
||||
label: "Shared activity".into(),
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
doc.execute(state.prepare_pending(doc.project()).unwrap())
|
||||
.unwrap();
|
||||
let added = created(&f.p, doc.project(), f.owner);
|
||||
let mut expected = old_order;
|
||||
expected.insert(index, added);
|
||||
assert_eq!(doc.project().elements[&f.owner].steps().unwrap(), &expected);
|
||||
let links = sequence_links(doc.project(), f.owner);
|
||||
assert_eq!(links.len(), 3);
|
||||
for pair in expected.windows(2) {
|
||||
assert_eq!(
|
||||
links
|
||||
.iter()
|
||||
.filter(|r| r.source() == pair[0] && r.target() == pair[1])
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
}
|
||||
for (id, relation) in &f.p.relations {
|
||||
assert_eq!(&doc.project().relations[id], relation);
|
||||
}
|
||||
let after = doc.bundle().clone();
|
||||
assert!(doc.undo());
|
||||
assert_eq!(doc.bundle(), &before);
|
||||
assert_eq!(doc.project(), &f.p);
|
||||
assert!(doc.redo());
|
||||
assert_eq!(doc.bundle(), &after);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_process_incompatible_imported_order_rejects_conversion_without_losing_draft() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let (mut f, _) = empty_exchange_fixture(layer);
|
||||
// Repeated exchange uses have different occurrences, but this legacy path
|
||||
// has an endpoint gap. Converting its implied adjacency must reject.
|
||||
for _ in 0..2 {
|
||||
let node = occurrence(
|
||||
&f.p,
|
||||
f.owner,
|
||||
"Repeated exchange",
|
||||
OccurrenceBody::Exchange {
|
||||
operation: None,
|
||||
exchange: Some(f.exchange),
|
||||
sender: None,
|
||||
receiver: None,
|
||||
message_kind: MessageKind::default(),
|
||||
reply_to: None,
|
||||
values: vec![],
|
||||
},
|
||||
);
|
||||
f.p = execute_batch(&f.p, &[Command::CreateElement(node)]).unwrap();
|
||||
}
|
||||
let mut doc = Document::from_project(f.p.clone(), ProjectLimits::default()).unwrap();
|
||||
let before = doc.bundle().clone();
|
||||
assert_interactive_owner_view(doc.project(), f.owner);
|
||||
assert_eq!(
|
||||
Graph::new(doc.project(), f.owner, f.owner)
|
||||
.ordered_paths
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
let mut state = State::default();
|
||||
state
|
||||
.begin(
|
||||
doc.project(),
|
||||
context(f.owner),
|
||||
Tool::Control,
|
||||
ControlKind::Choice,
|
||||
Insertion::default(),
|
||||
)
|
||||
.unwrap();
|
||||
state
|
||||
.update(
|
||||
doc.project(),
|
||||
Event::Name("Retain this proposed decision".into()),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(state.prepare_pending(doc.project()).is_err());
|
||||
assert_eq!(
|
||||
state.draft.as_ref().unwrap().name,
|
||||
"Retain this proposed decision"
|
||||
);
|
||||
assert_eq!(doc.bundle(), &before);
|
||||
assert_eq!(doc.project(), &f.p);
|
||||
assert!(!doc.undo(), "rejection must not create history");
|
||||
state.update(doc.project(), Event::Cancel).unwrap();
|
||||
assert!(state.draft.is_none());
|
||||
assert_eq!(doc.bundle(), &before);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_process_empty_first_and_second_exchange_keep_the_same_interactive_renderer() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let (f, next) = empty_exchange_fixture(layer);
|
||||
let mut doc = Document::from_project(f.p.clone(), ProjectLimits::default()).unwrap();
|
||||
for (index, target) in [f.exchange, next].into_iter().enumerate() {
|
||||
assert_interactive_owner_view(doc.project(), f.owner);
|
||||
let before = doc.bundle().clone();
|
||||
let mut graph = Graph::new(doc.project(), f.owner, f.owner);
|
||||
let c = context(f.owner);
|
||||
graph.interaction = Some((
|
||||
c,
|
||||
CanvasTool::Add(Tool::Exchange, ControlKind::Choice),
|
||||
true,
|
||||
));
|
||||
let mut input = Input::default();
|
||||
let bounds = Rectangle::with_size(graph.size);
|
||||
let point = Point::new(graph.size.width - 10., graph.size.height - 10.);
|
||||
send(
|
||||
&graph,
|
||||
&mut input,
|
||||
mouse::Event::ButtonPressed(mouse::Button::Left),
|
||||
point,
|
||||
bounds,
|
||||
);
|
||||
let Some(Message::TopologyCanvas(CanvasEvent::Begin { insertion, .. })) = send(
|
||||
&graph,
|
||||
&mut input,
|
||||
mouse::Event::ButtonReleased(mouse::Button::Left),
|
||||
point,
|
||||
bounds,
|
||||
) else {
|
||||
panic!("canvas retains direct exchange insertion");
|
||||
};
|
||||
let mut state = State::default();
|
||||
state
|
||||
.begin(
|
||||
doc.project(),
|
||||
c,
|
||||
Tool::Exchange,
|
||||
ControlKind::Choice,
|
||||
insertion,
|
||||
)
|
||||
.unwrap();
|
||||
state
|
||||
.update(
|
||||
doc.project(),
|
||||
Event::Target(Choice {
|
||||
id: target,
|
||||
label: "Exchange".into(),
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
doc.execute(state.prepare_pending(doc.project()).unwrap())
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
doc.project().elements[&f.owner].steps().unwrap().len(),
|
||||
index + 1
|
||||
);
|
||||
assert_eq!(sequence_links(doc.project(), f.owner).len(), index);
|
||||
assert_interactive_owner_view(doc.project(), f.owner);
|
||||
let after = doc.bundle().clone();
|
||||
assert!(doc.undo());
|
||||
assert_eq!(doc.bundle(), &before);
|
||||
assert!(doc.redo());
|
||||
assert_eq!(doc.bundle(), &after);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_process_imported_order_is_projected_without_mutation_and_first_canvas_edit_is_atomic() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let (mut f, next) = empty_exchange_fixture(layer);
|
||||
for exchange in [f.exchange, next] {
|
||||
let node = occurrence(
|
||||
&f.p,
|
||||
f.owner,
|
||||
"Imported exchange",
|
||||
OccurrenceBody::Exchange {
|
||||
operation: None,
|
||||
exchange: Some(exchange),
|
||||
sender: None,
|
||||
receiver: None,
|
||||
message_kind: MessageKind::default(),
|
||||
reply_to: None,
|
||||
values: vec![],
|
||||
},
|
||||
);
|
||||
f.p = execute_batch(&f.p, &[Command::CreateElement(node)]).unwrap();
|
||||
}
|
||||
let mut doc = Document::from_project(f.p.clone(), ProjectLimits::default()).unwrap();
|
||||
let before = doc.bundle().clone();
|
||||
let order = legacy_order(doc.project(), f.owner);
|
||||
assert_eq!(order.len(), 2);
|
||||
assert_interactive_owner_view(doc.project(), f.owner);
|
||||
let mut graph = Graph::new(doc.project(), f.owner, order[0]);
|
||||
assert!(
|
||||
graph.edges.is_empty(),
|
||||
"projection never invents relation identities"
|
||||
);
|
||||
assert_eq!(graph.ordered_paths.len(), 1);
|
||||
assert!(position(&graph, order[0]).x < position(&graph, order[1]).x);
|
||||
let points = &graph.ordered_paths[0].points;
|
||||
let point = Point::new((points[0].x + points[1].x) / 2., points[0].y);
|
||||
let c = context(f.owner);
|
||||
graph.interaction = Some((c, CanvasTool::Add(Tool::Work, ControlKind::Choice), true));
|
||||
let mut input = Input::default();
|
||||
let bounds = Rectangle::with_size(graph.size);
|
||||
send(
|
||||
&graph,
|
||||
&mut input,
|
||||
mouse::Event::ButtonPressed(mouse::Button::Left),
|
||||
point,
|
||||
bounds,
|
||||
);
|
||||
let Some(Message::TopologyCanvas(CanvasEvent::Begin { insertion, .. })) = send(
|
||||
&graph,
|
||||
&mut input,
|
||||
mouse::Event::ButtonReleased(mouse::Button::Left),
|
||||
point,
|
||||
bounds,
|
||||
) else {
|
||||
panic!("ordered arrow supports direct insertion");
|
||||
};
|
||||
assert_eq!(
|
||||
insertion,
|
||||
Insertion {
|
||||
link: None,
|
||||
before: Some(order[1])
|
||||
}
|
||||
);
|
||||
assert_eq!(doc.bundle(), &before);
|
||||
let mut state = State::default();
|
||||
state
|
||||
.begin(doc.project(), c, Tool::Work, ControlKind::Choice, insertion)
|
||||
.unwrap();
|
||||
state
|
||||
.update(
|
||||
doc.project(),
|
||||
Event::Target(Choice {
|
||||
id: f.work[1],
|
||||
label: "Respond".into(),
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
doc.execute(state.prepare_pending(doc.project()).unwrap())
|
||||
.unwrap();
|
||||
let inserted = created(&f.p, doc.project(), f.owner);
|
||||
assert_eq!(
|
||||
doc.project().elements[&f.owner].steps().unwrap(),
|
||||
&[order[0], inserted, order[1]]
|
||||
);
|
||||
let links = sequence_links(doc.project(), f.owner);
|
||||
assert_eq!(links.len(), 2);
|
||||
for (a, b) in [(order[0], inserted), (inserted, order[1])] {
|
||||
assert!(
|
||||
links
|
||||
.iter()
|
||||
.any(|link| link.source() == a && link.target() == b)
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
Graph::new(doc.project(), f.owner, inserted)
|
||||
.ordered_paths
|
||||
.is_empty()
|
||||
);
|
||||
for id in f.work {
|
||||
assert_eq!(doc.project().elements[&id], f.p.elements[&id]);
|
||||
}
|
||||
assert!(doc.undo());
|
||||
assert_eq!(doc.bundle(), &before);
|
||||
assert_eq!(doc.project(), &f.p);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_process_palette_preserves_fit_and_manual_camera() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
|
||||
@@ -59,21 +59,16 @@ pub(super) fn choice_page(len: usize, requested: usize) -> std::ops::Range<usize
|
||||
let start = requested.min(len.saturating_sub(1) / 24) * 24;
|
||||
start..(start + 24).min(len)
|
||||
}
|
||||
pub(super) fn view<'a>(
|
||||
app: &'a App,
|
||||
owner: Id,
|
||||
mut out: Column<'a, Message>,
|
||||
) -> Element<'a, Message> {
|
||||
pub(super) fn view<'a>(app: &'a App, owner: Id, out: Column<'a, Message>) -> Element<'a, Message> {
|
||||
let p = app.project();
|
||||
let context = app.topology_context().expect("visible process context");
|
||||
let state = &app.behavior.topology;
|
||||
let pending = state.draft.is_some();
|
||||
let explicit =
|
||||
uses_explicit_topology(p, owner) || p.elements[&owner].steps().is_none_or(Vec::is_empty);
|
||||
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 && explicit {
|
||||
if !pending {
|
||||
controls = controls
|
||||
.push(
|
||||
row![
|
||||
@@ -156,10 +151,22 @@ pub(super) fn view<'a>(
|
||||
.spacing(6),
|
||||
);
|
||||
}
|
||||
controls=controls.push(label(if pending{"Pending contribution · apply or cancel below"}else if !explicit{"Ordered contributions · Append exchange continues the path"}else{match state.tool{
|
||||
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))),
|
||||
);
|
||||
}
|
||||
}
|
||||
// A fixed sibling keeps Iced's canvas scroll state through selection/form changes.
|
||||
let mut navigation = column![].spacing(6);
|
||||
if let Some(Relation {
|
||||
@@ -249,7 +256,7 @@ pub(super) fn view<'a>(
|
||||
button("Back to caller").on_press(Message::TopologyCanvas(CanvasEvent::Back(context))),
|
||||
);
|
||||
}
|
||||
let diagram = if explicit {
|
||||
let diagram = {
|
||||
let mut graph = Graph::expanded_qualified(
|
||||
p,
|
||||
owner,
|
||||
@@ -373,58 +380,6 @@ pub(super) fn view<'a>(
|
||||
state.shelf.is_some(),
|
||||
pending,
|
||||
)
|
||||
} else {
|
||||
out = out
|
||||
.push(
|
||||
button(if state.focused {
|
||||
"Restore explorer and properties"
|
||||
} else {
|
||||
"Focus canvas"
|
||||
})
|
||||
.on_press(Message::TopologyCanvas(CanvasEvent::Focus(context))),
|
||||
)
|
||||
.push(controls)
|
||||
.push(navigation);
|
||||
out = out.push(label(
|
||||
"Ordered exchange path · append the next exchange or convert to explicit links.",
|
||||
));
|
||||
if !pending {
|
||||
out = out.push(
|
||||
button("Append exchange…")
|
||||
.on_press(Message::TopologyCanvas(CanvasEvent::AppendExchange(context))),
|
||||
).push(label("Append creates the next exchange contribution and an explicit sequence link from the final contribution in one undo step."));
|
||||
}
|
||||
let commands = preserved_order(p, owner, None);
|
||||
if !commands.is_empty() {
|
||||
out = out.push(
|
||||
button("Convert ordered path to explicit links")
|
||||
.on_press(Message::Command(unique_batch(p, commands))),
|
||||
);
|
||||
} else {
|
||||
out=out.push(label("A single exchange has no path link to convert. Append the next exchange, or involve work, a control or a reusable process."))
|
||||
.push(row![button("Add work…").on_press(event(Event::Start(owner,Tool::Work,selected))),
|
||||
button("Add control…").on_press(event(Event::Start(owner,Tool::Control,selected))),
|
||||
button("Reuse…").on_press(event(Event::Start(owner,Tool::Reuse,selected)))].spacing(6));
|
||||
}
|
||||
let scene = Scene::sequence(p, owner, app.selected);
|
||||
let size = scene.size;
|
||||
out = out.push(
|
||||
scrollable(
|
||||
canvas::Canvas::new(scene)
|
||||
.width(size.width)
|
||||
.height(size.height),
|
||||
)
|
||||
.direction(scrollable::Direction::Both {
|
||||
vertical: scrollable::Scrollbar::default(),
|
||||
horizontal: scrollable::Scrollbar::default(),
|
||||
})
|
||||
.height(240.),
|
||||
);
|
||||
super::super::scenario_scroll::observe_topology(
|
||||
context,
|
||||
scenario::ScrollArea::Page,
|
||||
scrollable(out.padding(18)),
|
||||
)
|
||||
};
|
||||
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) {
|
||||
@@ -451,6 +406,17 @@ pub(super) fn view<'a>(
|
||||
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
|
||||
|
||||
@@ -621,8 +621,9 @@ impl canvas::Program<Message> for DiagramCanvas<'_> {
|
||||
Key::Named(Named::F2)
|
||||
if self.editable && self.connection.is_none() && modifiers.is_empty() =>
|
||||
{
|
||||
let view = self.scene(state).view;
|
||||
let node = view
|
||||
let scene = self.scene(state);
|
||||
let node = scene
|
||||
.view
|
||||
.nodes
|
||||
.iter()
|
||||
.find(|n| n.element == self.selected || n.id == self.selected)?;
|
||||
|
||||
@@ -16,6 +16,7 @@ use serde_json::{Value, json};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
mod context_fields;
|
||||
mod data_fields;
|
||||
mod history_fields;
|
||||
mod port_fields;
|
||||
mod view_fields;
|
||||
|
||||
@@ -50,6 +51,14 @@ pub(crate) fn set_shape_value(record: &mut Value, path: &str, value: Value) -> R
|
||||
*target = value;
|
||||
return Ok(());
|
||||
}
|
||||
if history_fields::is_path(path)
|
||||
&& history_fields::has_field(path)
|
||||
&& let Some((parent, field)) = path.rsplit_once('/')
|
||||
&& let Some(fields) = record.pointer_mut(parent).and_then(Value::as_object_mut)
|
||||
{
|
||||
fields.insert(field.into(), value);
|
||||
return Ok(());
|
||||
}
|
||||
// Ordinary messages omit operation=None in source/JSON. Authoring that one
|
||||
// optional field must not require changing the historic record encoding.
|
||||
if let Some(parent) = path.strip_suffix("/operation")
|
||||
@@ -75,6 +84,7 @@ pub struct Draft {
|
||||
port_contract_drafts: BTreeMap<String, Value>,
|
||||
pub value_browsers: BTreeMap<String, ValueBrowser>,
|
||||
pub reference_browsers: BTreeMap<String, ReferenceBrowser>,
|
||||
history: history_fields::State,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
@@ -156,6 +166,7 @@ impl Draft {
|
||||
})
|
||||
.collect();
|
||||
draft.port_contract_drafts = saved.ports;
|
||||
draft.history.open = draft.kind == RecordKind::Trace;
|
||||
Ok(draft)
|
||||
}
|
||||
pub fn from_project(project: &Project, selected: Id) -> Option<Self> {
|
||||
@@ -197,6 +208,7 @@ impl Draft {
|
||||
port_contract_drafts: BTreeMap::new(),
|
||||
value_browsers: BTreeMap::new(),
|
||||
reference_browsers: BTreeMap::new(),
|
||||
history: Default::default(),
|
||||
};
|
||||
draft.rebuild();
|
||||
Some(draft)
|
||||
@@ -311,9 +323,38 @@ impl Draft {
|
||||
.map_or(true, |value| value != self.baseline)
|
||||
}
|
||||
pub fn set_exact(&mut self, enabled: bool) -> Result<(), String> {
|
||||
self.value = self.materialize()?;
|
||||
let value = self.materialize()?;
|
||||
let mut history_inputs = Vec::new();
|
||||
if enabled != self.exact_strings {
|
||||
for (key, (_, inputs)) in &self.constructor_drafts {
|
||||
if self.kind != RecordKind::Trace || !history_fields::is_path(&key.0) {
|
||||
continue;
|
||||
}
|
||||
for (path, input) in inputs {
|
||||
if free_string(path) {
|
||||
let raw = if enabled {
|
||||
serde_json::to_string(&input.raw).expect("String serialization")
|
||||
} else {
|
||||
serde_json::from_str::<String>(&input.raw).map_err(|_| "Repair the quoted text in the retained history constructor before changing escaped-string mode.".to_owned())?
|
||||
};
|
||||
history_inputs.push((key.clone(), path.clone(), raw));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.value = value;
|
||||
self.exact_strings = enabled;
|
||||
self.rebuild();
|
||||
for (key, path, raw) in history_inputs {
|
||||
if let Some(input) = self
|
||||
.constructor_drafts
|
||||
.get_mut(&key)
|
||||
.and_then(|(_, inputs)| inputs.get_mut(&path))
|
||||
{
|
||||
input.editor = text_editor::Content::with_text(&raw);
|
||||
input.raw = raw;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub fn input(&mut self, path: &str, value: String) {
|
||||
@@ -329,6 +370,29 @@ impl Draft {
|
||||
}
|
||||
}
|
||||
pub fn shape(&mut self, path: &str, value: Value) -> Result<(), String> {
|
||||
if self.kind == RecordKind::Trace {
|
||||
if path == history_fields::TOGGLE {
|
||||
self.history.open = value
|
||||
.as_bool()
|
||||
.ok_or("Choose whether to show stored history.")?;
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(path) = path.strip_prefix(history_fields::PAGE) {
|
||||
self.history.pages.insert(
|
||||
path.into(),
|
||||
value.as_u64().ok_or("Choose a history page.")? as usize,
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(path) = path.strip_prefix(history_fields::VARIANT) {
|
||||
return self.variant_scoped(path, value);
|
||||
}
|
||||
if history_fields::is_path(path) {
|
||||
self.constructor_drafts
|
||||
.retain(|(root, _), _| !within(root, path));
|
||||
return self.shape_scoped(path, value);
|
||||
}
|
||||
}
|
||||
let mut candidate = self.materialize()?;
|
||||
let value = if path.ends_with("/Port/contract") {
|
||||
port_fields::switch_contract(
|
||||
@@ -369,7 +433,8 @@ impl Draft {
|
||||
|| path.ends_with("/Occurrence/body")
|
||||
|| path.ends_with("/Event/body")
|
||||
|| path.ends_with("/message_kind")
|
||||
|| path.ends_with("/ContextualLink/purpose");
|
||||
|| path.ends_with("/ContextualLink/purpose")
|
||||
|| (self.kind == RecordKind::Trace && history_fields::is_path(path));
|
||||
if !constructor {
|
||||
return self.shape_scoped(path, proposed);
|
||||
}
|
||||
@@ -384,6 +449,9 @@ impl Draft {
|
||||
None
|
||||
};
|
||||
let tag = |value: &Value| {
|
||||
if self.kind == RecordKind::Trace && history_fields::is_path(path) {
|
||||
return history_fields::constructor_tag(path, value);
|
||||
}
|
||||
value
|
||||
.as_str()
|
||||
.or_else(|| value.as_object()?.keys().next().map(String::as_str))
|
||||
@@ -448,7 +516,7 @@ impl Draft {
|
||||
.len();
|
||||
edit_array(&mut candidate, path, index, direction)?;
|
||||
self.value = candidate;
|
||||
self.rebuild_retaining(|old| {
|
||||
let relocate = |old: &str| {
|
||||
let Some(tail) = old.strip_prefix(&format!("{path}/")) else {
|
||||
return Some(old.to_owned());
|
||||
};
|
||||
@@ -474,7 +542,24 @@ impl Draft {
|
||||
} else {
|
||||
format!("{path}/{position}/{rest}")
|
||||
})
|
||||
});
|
||||
};
|
||||
self.rebuild_retaining(relocate);
|
||||
if self.kind == RecordKind::Trace && history_fields::is_path(path) {
|
||||
let previous = std::mem::take(&mut self.constructor_drafts);
|
||||
for ((root, tag), (value, inputs)) in previous {
|
||||
if !root.starts_with(&format!("{path}/")) {
|
||||
self.constructor_drafts.insert((root, tag), (value, inputs));
|
||||
continue;
|
||||
}
|
||||
if let Some(root) = relocate(&root) {
|
||||
let inputs = inputs
|
||||
.into_iter()
|
||||
.filter_map(|(path, input)| relocate(&path).map(|path| (path, input)))
|
||||
.collect();
|
||||
self.constructor_drafts.insert((root, tag), (value, inputs));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub fn append_scoped(&mut self, path: &str, value: Value) -> Result<(), String> {
|
||||
@@ -510,6 +595,9 @@ impl Draft {
|
||||
index: Option<usize>,
|
||||
direction: i32,
|
||||
) -> Result<(), String> {
|
||||
if self.kind == RecordKind::Trace && history_fields::is_path(path) {
|
||||
return self.array_scoped(path, index, direction);
|
||||
}
|
||||
let mut candidate = self.materialize()?;
|
||||
edit_array(&mut candidate, path, index, direction)?;
|
||||
self.value = candidate;
|
||||
@@ -517,6 +605,9 @@ impl Draft {
|
||||
Ok(())
|
||||
}
|
||||
pub fn append(&mut self, path: &str, value: Value) -> Result<(), String> {
|
||||
if self.kind == RecordKind::Trace && history_fields::is_path(path) {
|
||||
return self.append_scoped(path, value);
|
||||
}
|
||||
let mut candidate = self.materialize()?;
|
||||
candidate
|
||||
.pointer_mut(path)
|
||||
@@ -625,6 +716,9 @@ fn edit_array(
|
||||
index: Option<usize>,
|
||||
direction: i32,
|
||||
) -> Result<(), String> {
|
||||
if history_fields::is_path(path) && index.is_none() {
|
||||
return Err("Choose a typed stored fact or list value before adding it.".into());
|
||||
}
|
||||
let items = value
|
||||
.pointer_mut(path)
|
||||
.and_then(Value::as_array_mut)
|
||||
@@ -807,6 +901,9 @@ fn multiline(path: &str) -> bool {
|
||||
)
|
||||
}
|
||||
fn free_string(path: &str) -> bool {
|
||||
if history_fields::is_path(path) {
|
||||
return history_fields::free_string(path);
|
||||
}
|
||||
multiline(path)
|
||||
|| matches!(
|
||||
path.rsplit('/').next(),
|
||||
@@ -1728,6 +1825,9 @@ fn property<'a>(
|
||||
label: &str,
|
||||
skip_variant: bool,
|
||||
) -> View<'a, Message> {
|
||||
if draft.kind == RecordKind::Trace && path == "/baseline" {
|
||||
return history_fields::view(draft, project, value);
|
||||
}
|
||||
if let Some(view) = context_fields::property(draft, project, value, path) {
|
||||
return view;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,705 @@
|
||||
//! Typed editing of stored observations, independently of the live counterparts.
|
||||
//! The checked-in, contract-tested OpenAPI descriptor supplies only field shapes.
|
||||
//! Serde and core historical validation remain the admission authority.
|
||||
use super::*;
|
||||
use iced::widget::column;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
pub(super) const TOGGLE: &str = "/$stored_history";
|
||||
pub(super) const PAGE: &str = "/$stored_history_page";
|
||||
pub(super) const VARIANT: &str = "/$stored_history_variant";
|
||||
const ROOT: &str = "/baseline";
|
||||
const FACETS: &str = "/baseline/semantic/facets";
|
||||
const PAGE_SIZE: usize = 12;
|
||||
const MAX_DEPTH: usize = 32;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct State {
|
||||
pub open: bool,
|
||||
pub pages: BTreeMap<String, usize>,
|
||||
}
|
||||
|
||||
pub(super) fn is_path(path: &str) -> bool {
|
||||
path == ROOT || path.starts_with("/baseline/")
|
||||
}
|
||||
|
||||
fn api() -> &'static Value {
|
||||
static API: OnceLock<Value> = OnceLock::new();
|
||||
API.get_or_init(|| {
|
||||
serde_json::from_str(include_str!("../../../../docs/openapi.json"))
|
||||
.expect("The bundled OpenAPI descriptor is valid JSON")
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve(mut schema: &'static Value) -> Result<&'static Value, &'static str> {
|
||||
for _ in 0..MAX_DEPTH {
|
||||
let Some(reference) = schema.get("$ref").and_then(Value::as_str) else {
|
||||
return Ok(schema);
|
||||
};
|
||||
schema = reference
|
||||
.strip_prefix('#')
|
||||
.and_then(|path| api().pointer(path))
|
||||
.ok_or("Stored-history descriptor has an unresolved local reference.")?;
|
||||
}
|
||||
Err("Stored-history descriptor reference depth exceeded.")
|
||||
}
|
||||
|
||||
fn named(name: &str) -> Result<&'static Value, &'static str> {
|
||||
api()["components"]["schemas"]
|
||||
.get(name)
|
||||
.ok_or("Stored-history descriptor is missing a required type.")
|
||||
}
|
||||
|
||||
fn alternatives(schema: &'static Value) -> Option<&'static Vec<Value>> {
|
||||
schema
|
||||
.get("oneOf")
|
||||
.or_else(|| schema.get("anyOf"))
|
||||
.and_then(Value::as_array)
|
||||
}
|
||||
|
||||
fn at_schema(schema: &'static Value, parts: &[&str], depth: usize) -> Option<&'static Value> {
|
||||
if depth > MAX_DEPTH {
|
||||
return None;
|
||||
}
|
||||
let schema = resolve(schema).ok()?;
|
||||
let Some((part, rest)) = parts.split_first() else {
|
||||
return Some(schema);
|
||||
};
|
||||
if let Some(options) = alternatives(schema) {
|
||||
return options.iter().find_map(|s| at_schema(s, parts, depth + 1));
|
||||
}
|
||||
match schema.get("type").and_then(Value::as_str) {
|
||||
Some("object") => at_schema(schema.get("properties")?.get(*part)?, rest, depth + 1),
|
||||
Some("array") if part.parse::<usize>().is_ok() => {
|
||||
at_schema(schema.get("items")?, rest, depth + 1)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn schema_at(path: &str) -> Option<&'static Value> {
|
||||
let tail = path.strip_prefix(ROOT)?;
|
||||
let parts: Vec<_> = tail
|
||||
.trim_start_matches('/')
|
||||
.split('/')
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
at_schema(named("TraceBaseline").ok()?, &parts, 0)
|
||||
}
|
||||
|
||||
pub(super) fn has_field(path: &str) -> bool {
|
||||
schema_at(path).is_some()
|
||||
}
|
||||
|
||||
pub(super) fn free_string(path: &str) -> bool {
|
||||
fn text_shape(schema: &'static Value, depth: usize) -> bool {
|
||||
if depth > MAX_DEPTH {
|
||||
return false;
|
||||
}
|
||||
let Ok(schema) = resolve(schema) else {
|
||||
return false;
|
||||
};
|
||||
if let Some(options) = alternatives(schema) {
|
||||
return options.iter().any(|s| text_shape(s, depth + 1));
|
||||
}
|
||||
schema.get("type").and_then(Value::as_str) == Some("string")
|
||||
&& schema.get("enum").is_none()
|
||||
&& schema.get("format").and_then(Value::as_str) != Some("uuid")
|
||||
}
|
||||
schema_at(path).is_some_and(|s| text_shape(s, 0))
|
||||
}
|
||||
|
||||
/// A shape seed is a draft, never an acknowledged historical fact. UUIDs start
|
||||
/// empty. Observed starts NA on both sides, which core rejects. Typed defaults
|
||||
/// inside Value are introduced only after the engineer chooses that branch.
|
||||
fn seed(schema: &'static Value, depth: usize) -> Result<Value, &'static str> {
|
||||
if depth > MAX_DEPTH {
|
||||
return Err("Stored-history constructor depth exceeded.");
|
||||
}
|
||||
let schema = resolve(schema)?;
|
||||
if let Some(options) = alternatives(schema) {
|
||||
if options
|
||||
.iter()
|
||||
.any(|s| matches_value(s, &json!("NotApplicable"), 0))
|
||||
{
|
||||
return Ok(json!("NotApplicable"));
|
||||
}
|
||||
if options.iter().any(|s| matches_value(s, &Value::Null, 0)) {
|
||||
return Ok(Value::Null);
|
||||
}
|
||||
return seed(
|
||||
options
|
||||
.first()
|
||||
.ok_or("Stored-history union has no constructors.")?,
|
||||
depth + 1,
|
||||
);
|
||||
}
|
||||
if let Some(values) = schema.get("enum").and_then(Value::as_array) {
|
||||
return values
|
||||
.first()
|
||||
.cloned()
|
||||
.ok_or("Stored-history enumeration has no values.");
|
||||
}
|
||||
Ok(match schema.get("type").and_then(Value::as_str) {
|
||||
Some("object") => {
|
||||
let fields = schema
|
||||
.get("properties")
|
||||
.and_then(Value::as_object)
|
||||
.ok_or("Stored-history object has no closed field descriptor.")?;
|
||||
let mut result = serde_json::Map::new();
|
||||
for (name, schema) in fields {
|
||||
result.insert(name.clone(), seed(schema, depth + 1)?);
|
||||
}
|
||||
Value::Object(result)
|
||||
}
|
||||
Some("array") => json!([]),
|
||||
Some("boolean") => json!(false),
|
||||
Some("integer") => json!(0),
|
||||
Some("number") => json!(0.0),
|
||||
Some("string") => json!(""),
|
||||
Some("null") => Value::Null,
|
||||
_ => return Err("Unsupported stored-history field descriptor."),
|
||||
})
|
||||
}
|
||||
|
||||
/// Shape matching selects the displayed constructor; it is not validation.
|
||||
/// Required values may be incomplete while their chosen branch stays editable.
|
||||
fn matches_value(schema: &'static Value, value: &Value, depth: usize) -> bool {
|
||||
if depth > MAX_DEPTH {
|
||||
return false;
|
||||
}
|
||||
let Ok(schema) = resolve(schema) else {
|
||||
return false;
|
||||
};
|
||||
if let Some(options) = alternatives(schema) {
|
||||
return options.iter().any(|s| matches_value(s, value, depth + 1));
|
||||
}
|
||||
if let Some(choices) = schema.get("enum").and_then(Value::as_array) {
|
||||
return choices.contains(value);
|
||||
}
|
||||
match schema.get("type").and_then(Value::as_str) {
|
||||
Some("object") => value.as_object().is_some_and(|fields| {
|
||||
schema
|
||||
.get("properties")
|
||||
.and_then(Value::as_object)
|
||||
.is_some_and(|properties| {
|
||||
fields.keys().all(|key| properties.contains_key(key))
|
||||
&& schema
|
||||
.get("required")
|
||||
.and_then(Value::as_array)
|
||||
.is_none_or(|required| {
|
||||
required
|
||||
.iter()
|
||||
.filter_map(Value::as_str)
|
||||
.all(|key| fields.contains_key(key))
|
||||
})
|
||||
})
|
||||
}),
|
||||
Some("array") => value.is_array(),
|
||||
Some("boolean") => value.is_boolean(),
|
||||
Some("integer" | "number") => value.is_number(),
|
||||
Some("string") => value.is_string(),
|
||||
Some("null") => value.is_null(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn label(name: &str) -> String {
|
||||
match name {
|
||||
"oa" => return "Operational observation".into(),
|
||||
"sa" => return "System observation".into(),
|
||||
"NotApplicable" => return "Not applicable".into(),
|
||||
"Value" => return "Observed value".into(),
|
||||
"keys" => return "Exact historical keys".into(),
|
||||
"review" => return "Stored observations".into(),
|
||||
_ => {}
|
||||
}
|
||||
let mut out = String::new();
|
||||
let mut previous_lower = false;
|
||||
for c in name.chars() {
|
||||
if c == '_' {
|
||||
out.push(' ');
|
||||
} else {
|
||||
if c.is_uppercase() && previous_lower {
|
||||
out.push(' ');
|
||||
}
|
||||
out.push(c);
|
||||
}
|
||||
previous_lower = c.is_lowercase();
|
||||
}
|
||||
let mut chars = out.chars();
|
||||
chars
|
||||
.next()
|
||||
.map(|c| c.to_uppercase().collect::<String>() + chars.as_str())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn branch_label(schema: &'static Value) -> String {
|
||||
let Ok(schema) = resolve(schema) else {
|
||||
return "Unavailable type".into();
|
||||
};
|
||||
if let Some(word) = schema
|
||||
.get("enum")
|
||||
.and_then(Value::as_array)
|
||||
.filter(|v| v.len() == 1)
|
||||
.and_then(|v| v[0].as_str())
|
||||
{
|
||||
return label(word);
|
||||
}
|
||||
if schema.get("enum").is_some() {
|
||||
return "Enumeration · choose below".into();
|
||||
}
|
||||
if let Some(fields) = schema.get("properties").and_then(Value::as_object)
|
||||
&& fields.len() == 1
|
||||
{
|
||||
return label(fields.keys().next().unwrap());
|
||||
}
|
||||
match schema.get("type").and_then(Value::as_str) {
|
||||
Some("null") => "Absent (none)".into(),
|
||||
Some("object") => "Set record".into(),
|
||||
Some("array") => "Set list".into(),
|
||||
Some("string") if schema.get("format").and_then(Value::as_str) == Some("uuid") => {
|
||||
"Set historical UUID".into()
|
||||
}
|
||||
Some("string") => "Set text".into(),
|
||||
Some("integer" | "number") => "Set number".into(),
|
||||
_ => "Set value / choose constructor".into(),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
struct FactChoice {
|
||||
label: String,
|
||||
value: Value,
|
||||
}
|
||||
impl std::fmt::Display for FactChoice {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(&self.label)
|
||||
}
|
||||
}
|
||||
|
||||
/// Flatten only the closed FacetReview selector levels, not observation values.
|
||||
/// Collection and incident-end choices are selectors too, so none is guessed.
|
||||
fn fact_choices() -> Result<&'static Vec<FactChoice>, &'static str> {
|
||||
static CHOICES: OnceLock<Result<Vec<FactChoice>, &'static str>> = OnceLock::new();
|
||||
CHOICES
|
||||
.get_or_init(|| {
|
||||
let root = resolve(named("FacetReview")?)?;
|
||||
let mut result = vec![];
|
||||
for branch in alternatives(root).ok_or("FacetReview must be a closed union.")? {
|
||||
let outer = resolve(branch)?;
|
||||
let (family, payload) = outer["properties"]
|
||||
.as_object()
|
||||
.and_then(|v| v.iter().next())
|
||||
.ok_or("Facet family is missing.")?;
|
||||
let payload = resolve(payload)?;
|
||||
if let Some(leaves) = alternatives(payload) {
|
||||
for leaf in leaves {
|
||||
let mut value = serde_json::Map::new();
|
||||
value.insert(family.clone(), seed(leaf, 0)?);
|
||||
result.push(FactChoice {
|
||||
label: format!("{} / {}", label(family), branch_label(leaf)),
|
||||
value: Value::Object(value),
|
||||
});
|
||||
}
|
||||
} else if family == "Artifact" {
|
||||
let leaves = alternatives(resolve(&payload["properties"]["review"])?)
|
||||
.ok_or("Artifact facts must be a closed union.")?;
|
||||
for leaf in leaves {
|
||||
let mut value = seed(branch, 0)?;
|
||||
value[family]["review"] = seed(leaf, 0)?;
|
||||
result.push(FactChoice {
|
||||
label: format!("Artifact / {}", branch_label(leaf)),
|
||||
value,
|
||||
});
|
||||
}
|
||||
} else if matches!(family.as_str(), "Member" | "Order" | "Incident") {
|
||||
let field = if family == "Incident" {
|
||||
"end"
|
||||
} else {
|
||||
"collection"
|
||||
};
|
||||
let choices = resolve(&payload["properties"][field])?["enum"]
|
||||
.as_array()
|
||||
.ok_or("Historical selector choices are missing.")?;
|
||||
for choice in choices {
|
||||
let mut value = seed(branch, 0)?;
|
||||
value[family][field] = choice.clone();
|
||||
result.push(FactChoice {
|
||||
label: format!(
|
||||
"{} / {}",
|
||||
label(family),
|
||||
label(choice.as_str().unwrap_or(""))
|
||||
),
|
||||
value,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
result.push(FactChoice {
|
||||
label: label(family),
|
||||
value: seed(branch, 0)?,
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
})
|
||||
.as_ref()
|
||||
.map_err(|error| *error)
|
||||
}
|
||||
|
||||
fn variant_message(path: &str, value: Value, present: bool) -> Message {
|
||||
Message::FieldShape(
|
||||
if present {
|
||||
format!("{VARIANT}{path}")
|
||||
} else {
|
||||
path.into()
|
||||
},
|
||||
value,
|
||||
)
|
||||
}
|
||||
|
||||
fn branch_seed(schema: &'static Value) -> Result<Value, &'static str> {
|
||||
let schema = resolve(schema)?;
|
||||
if let Some(values) = schema
|
||||
.get("enum")
|
||||
.and_then(Value::as_array)
|
||||
.filter(|v| v.len() == 1)
|
||||
{
|
||||
Ok(values[0].clone())
|
||||
} else {
|
||||
seed(schema, 0)
|
||||
}
|
||||
}
|
||||
|
||||
fn branch_has_fields(schema: &'static Value) -> bool {
|
||||
let Ok(schema) = resolve(schema) else {
|
||||
return false;
|
||||
};
|
||||
schema.get("type").and_then(Value::as_str) != Some("null")
|
||||
&& schema
|
||||
.get("enum")
|
||||
.and_then(Value::as_array)
|
||||
.is_none_or(|values| values.len() > 1)
|
||||
}
|
||||
|
||||
/// Constructor identity is independent of a primitive's edited content. In
|
||||
/// particular, absent, an empty UUID/text, a number and a list are different
|
||||
/// branches. String enum tags and externally tagged objects retain their variant.
|
||||
pub(super) fn constructor_tag(path: &str, value: &Value) -> String {
|
||||
fn enum_value(schema: &'static Value, value: &Value, depth: usize) -> bool {
|
||||
if depth > MAX_DEPTH {
|
||||
return false;
|
||||
}
|
||||
let Ok(schema) = resolve(schema) else {
|
||||
return false;
|
||||
};
|
||||
if let Some(options) = alternatives(schema) {
|
||||
return options
|
||||
.iter()
|
||||
.any(|option| enum_value(option, value, depth + 1));
|
||||
}
|
||||
schema
|
||||
.get("enum")
|
||||
.and_then(Value::as_array)
|
||||
.is_some_and(|values| values.contains(value))
|
||||
}
|
||||
match value {
|
||||
Value::Null => "null".into(),
|
||||
Value::Bool(_) => "boolean".into(),
|
||||
Value::Number(_) => "number".into(),
|
||||
Value::Array(_) => "array".into(),
|
||||
Value::String(word)
|
||||
if schema_at(path).is_some_and(|schema| enum_value(schema, value, 0)) =>
|
||||
{
|
||||
format!("enum:{word}")
|
||||
}
|
||||
Value::String(_) => "string".into(),
|
||||
Value::Object(fields) => format!(
|
||||
"object:{}",
|
||||
fields.keys().cloned().collect::<Vec<_>>().join("/")
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn paging<'a>(draft: &'a Draft, path: &str, count: usize) -> (usize, View<'a, Message>) {
|
||||
let last = count.saturating_sub(1) / PAGE_SIZE;
|
||||
let page = draft
|
||||
.history
|
||||
.pages
|
||||
.get(path)
|
||||
.copied()
|
||||
.unwrap_or(0)
|
||||
.min(last);
|
||||
let event = |page| Message::FieldShape(format!("{PAGE}{path}"), json!(page));
|
||||
let view = row![
|
||||
button("Previous").on_press_maybe((page > 0).then(|| event(page - 1))),
|
||||
text(format!(
|
||||
"{}–{} of {count}",
|
||||
if count == 0 { 0 } else { page * PAGE_SIZE + 1 },
|
||||
((page + 1) * PAGE_SIZE).min(count)
|
||||
))
|
||||
.size(11),
|
||||
button("Next").on_press_maybe((page < last).then(|| event(page + 1)))
|
||||
]
|
||||
.spacing(6)
|
||||
.align_y(iced::Center)
|
||||
.into();
|
||||
(page, view)
|
||||
}
|
||||
|
||||
fn render<'a>(
|
||||
draft: &'a Draft,
|
||||
project: &'a Project,
|
||||
schema: &'static Value,
|
||||
value: Option<&'a Value>,
|
||||
path: &str,
|
||||
title: &str,
|
||||
depth: usize,
|
||||
) -> View<'a, Message> {
|
||||
if depth > MAX_DEPTH {
|
||||
return text("Stored-history field nesting exceeds the supported editor depth.")
|
||||
.size(11)
|
||||
.into();
|
||||
}
|
||||
let schema = match resolve(schema) {
|
||||
Ok(s) => s,
|
||||
Err(e) => return text(e).size(11).into(),
|
||||
};
|
||||
let present = value.is_some();
|
||||
let value = value.unwrap_or(&Value::Null);
|
||||
if let Some(options) = alternatives(schema) {
|
||||
let initial: Vec<_> = match options.iter().map(branch_seed).collect::<Result<_, _>>() {
|
||||
Ok(values) => values,
|
||||
Err(error) => {
|
||||
return text(format!(
|
||||
"Stored-history constructor is unavailable: {error}"
|
||||
))
|
||||
.size(11)
|
||||
.into();
|
||||
}
|
||||
};
|
||||
let labels: Vec<_> = options.iter().map(branch_label).collect();
|
||||
let selected = options.iter().position(|s| matches_value(s, value, 0));
|
||||
let path_owned = path.to_string();
|
||||
let labels_owned = labels.clone();
|
||||
let mut out = column![
|
||||
text(title.to_owned()).size(11).color(style::MUTED),
|
||||
pick_list(
|
||||
labels,
|
||||
selected.map(|i| branch_label(&options[i])),
|
||||
move |choice| {
|
||||
let index = labels_owned.iter().position(|v| v == &choice).unwrap();
|
||||
variant_message(&path_owned, initial[index].clone(), present)
|
||||
}
|
||||
)
|
||||
.placeholder("Choose stored value / constructor…")
|
||||
.text_size(12)
|
||||
.padding(8)
|
||||
.width(Fill)
|
||||
]
|
||||
.spacing(6);
|
||||
if let Some(index) = selected {
|
||||
let branch = resolve(&options[index]).unwrap_or(&options[index]);
|
||||
if branch_has_fields(branch) {
|
||||
out = out.push(render(
|
||||
draft,
|
||||
project,
|
||||
branch,
|
||||
Some(value),
|
||||
path,
|
||||
"",
|
||||
depth + 1,
|
||||
));
|
||||
}
|
||||
}
|
||||
return out.into();
|
||||
}
|
||||
if let Some(words) = schema.get("enum").and_then(Value::as_array) {
|
||||
let choices: Vec<_> = words
|
||||
.iter()
|
||||
.filter_map(Value::as_str)
|
||||
.map(str::to_owned)
|
||||
.collect();
|
||||
let selected = value
|
||||
.as_str()
|
||||
.map(str::to_owned)
|
||||
.filter(|v| choices.contains(v));
|
||||
let path = path.to_string();
|
||||
return column![
|
||||
text(title.to_owned()).size(11).color(style::MUTED),
|
||||
pick_list(choices, selected, move |word| Message::FieldShape(
|
||||
path.clone(),
|
||||
json!(word)
|
||||
))
|
||||
.placeholder("Choose an explicit value…")
|
||||
.text_size(12)
|
||||
.padding(8)
|
||||
.width(Fill)
|
||||
]
|
||||
.spacing(6)
|
||||
.into();
|
||||
}
|
||||
match schema.get("type").and_then(Value::as_str) {
|
||||
Some("object") => {
|
||||
let Some(fields) = schema.get("properties").and_then(Value::as_object) else {
|
||||
return text("Unsupported stored-history object shape.").size(11).into();
|
||||
};
|
||||
let mut out = column![].spacing(8);
|
||||
if !title.is_empty() { out = out.push(text(title.to_owned()).size(12).color(style::INK)); }
|
||||
for (key, child_schema) in fields {
|
||||
out = out.push(render(draft, project, child_schema, value.get(key), &format!("{path}/{key}"), &label(key), depth + 1));
|
||||
}
|
||||
out.into()
|
||||
}
|
||||
Some("array") => {
|
||||
let Some(items) = value.as_array() else {
|
||||
return button("Initialize empty list").on_press(Message::FieldShape(path.into(), json!([]))).into();
|
||||
};
|
||||
let Some(item_schema) = schema.get("items") else { return text("Missing history list item type.").into(); };
|
||||
let mut out = column![text(title.to_owned()).size(12)].spacing(8);
|
||||
if path == FACETS {
|
||||
match fact_choices() {
|
||||
Ok(choices) => {
|
||||
let path = path.to_owned();
|
||||
out = out.push(pick_list(choices.clone(), None::<FactChoice>, move |choice| Message::FieldAppend(path.clone(), choice.value))
|
||||
.placeholder("Add: choose family and fact…").text_size(12).padding(8).width(Fill));
|
||||
}
|
||||
Err(error) => { out = out.push(text(error).size(11)); }
|
||||
}
|
||||
} else {
|
||||
match seed(item_schema, 0) {
|
||||
Ok(initial) => out = out.push(button("Add list value").on_press(Message::FieldAppend(path.into(), initial))),
|
||||
Err(error) => out = out.push(text(error).size(11)),
|
||||
}
|
||||
}
|
||||
let (page, pager) = paging(draft, path, items.len());
|
||||
if items.len() > PAGE_SIZE { out = out.push(pager); }
|
||||
for (index, item) in items.iter().enumerate().skip(page * PAGE_SIZE).take(PAGE_SIZE) {
|
||||
let item_path = format!("{path}/{index}");
|
||||
let mut card = column![render(draft, project, item_schema, Some(item), &item_path, &format!("{} {}", title, index + 1), depth + 1)].spacing(8);
|
||||
if path == FACETS {
|
||||
if let Some(current) = current_observation(draft, project, &item_path) {
|
||||
card = card.push(button("Use current counterpart observations")
|
||||
.on_press(Message::FieldShape(item_path.clone(), json!(current))).style(style::nav));
|
||||
}
|
||||
}
|
||||
card = card.push(row![
|
||||
button("↑").on_press_maybe((index > 0).then(|| Message::FieldArray(path.into(), Some(index), -1))),
|
||||
button("↓").on_press_maybe((index + 1 < items.len()).then(|| Message::FieldArray(path.into(), Some(index), 1))),
|
||||
button("Remove").on_press(Message::FieldArray(path.into(), Some(index), 0))
|
||||
].spacing(6));
|
||||
out = out.push(container(card).padding(10).style(style::subtle));
|
||||
}
|
||||
if items.is_empty() { out = out.push(text("Empty list · no stored entries").size(11).color(style::MUTED)); }
|
||||
out.into()
|
||||
}
|
||||
Some("boolean") => {
|
||||
let path = path.to_owned();
|
||||
checkbox(value.as_bool().unwrap_or(false)).label(title.to_owned())
|
||||
.on_toggle(move |v| Message::FieldShape(path.clone(), json!(v))).text_size(12).into()
|
||||
}
|
||||
Some("integer" | "number" | "string") => {
|
||||
let is_uuid = schema.get("format").and_then(Value::as_str) == Some("uuid");
|
||||
let numeric = schema.get("type").and_then(Value::as_str) != Some("string");
|
||||
let mut out = column![text(title.to_owned()).size(11).color(style::MUTED)].spacing(5);
|
||||
if let Some(input) = draft.inputs.get(path) {
|
||||
let owned = path.to_owned();
|
||||
if !numeric && !is_uuid && !draft.exact_strings {
|
||||
out = out.push(text_editor(&input.editor).on_action(move |a| Message::FieldText(owned.clone(), a)).size(12).height(72).padding(8));
|
||||
} else {
|
||||
out = out.push(text_input(if is_uuid { "Historical UUID, including deleted identities" } else { "Stored value" }, &input.raw)
|
||||
.on_input(move |raw| Message::Field(owned.clone(), raw)).size(12).padding(8));
|
||||
}
|
||||
} else {
|
||||
match seed(schema, 0) {
|
||||
Ok(value) => out = out.push(button("Set stored value").on_press(Message::FieldShape(path.into(), value))),
|
||||
Err(error) => out = out.push(text(error).size(11)),
|
||||
}
|
||||
}
|
||||
if is_uuid { out = out.push(text("Historical identity · existence in the current model is not required.").size(10).color(style::MUTED)); }
|
||||
out.into()
|
||||
}
|
||||
Some("null") => text("Absent (none)").size(11).into(),
|
||||
_ => text("Unsupported stored-history descriptor. The draft is retained; this field cannot be edited with these controls.").size(11).into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn current_observation(draft: &Draft, project: &Project, item_path: &str) -> Option<FacetReview> {
|
||||
let raw = draft
|
||||
.materialize_scopes(&[item_path.to_owned(), "/oa".into(), "/sa".into()])
|
||||
.ok()?;
|
||||
let fact: FacetReview = serde_json::from_value(raw.pointer(item_path)?.clone()).ok()?;
|
||||
let oa = raw.get("oa")?.as_str()?.parse().ok()?;
|
||||
let sa = raw.get("sa")?.as_str()?.parse().ok()?;
|
||||
semantic::capture_facet(project, oa, sa, &fact.selector()).ok()
|
||||
}
|
||||
|
||||
pub(super) fn view<'a>(
|
||||
draft: &'a Draft,
|
||||
project: &'a Project,
|
||||
value: &'a Value,
|
||||
) -> View<'a, Message> {
|
||||
let count = value
|
||||
.pointer("/semantic/facets")
|
||||
.and_then(Value::as_array)
|
||||
.map_or(0, Vec::len);
|
||||
let mut out = column![
|
||||
text("Stored review history").size(12).color(style::INK),
|
||||
text(if value.is_null() {
|
||||
"No stored baseline".into()
|
||||
} else {
|
||||
format!("Prose baseline · {count} semantic observations")
|
||||
})
|
||||
.size(11)
|
||||
.color(style::MUTED),
|
||||
button(if draft.history.open {
|
||||
"Close stored-history editor"
|
||||
} else {
|
||||
"Edit stored history (advanced)…"
|
||||
})
|
||||
.on_press(Message::FieldShape(
|
||||
TOGGLE.into(),
|
||||
json!(!draft.history.open)
|
||||
))
|
||||
.style(style::nav)
|
||||
]
|
||||
.spacing(8);
|
||||
if !draft.history.open {
|
||||
return out.into();
|
||||
}
|
||||
out = out.push(text("These edits change the trace's stored observations only. They do not change either counterpart. Ordinary Keep/Take review remains available from Traceability.").size(11));
|
||||
if value.is_null() {
|
||||
return match named("TraceBaseline").and_then(|s| seed(s, 0)) {
|
||||
Ok(initial) => out
|
||||
.push(
|
||||
button("Start empty stored baseline")
|
||||
.on_press(Message::FieldShape(ROOT.into(), initial)),
|
||||
)
|
||||
.into(),
|
||||
Err(error) => out.push(text(error).size(11)).into(),
|
||||
};
|
||||
}
|
||||
out = out.push(
|
||||
button("Clear whole stored baseline")
|
||||
.on_press(Message::FieldShape(ROOT.into(), Value::Null))
|
||||
.style(style::nav),
|
||||
);
|
||||
match named("TraceBaseline") {
|
||||
Ok(schema) => out = out.push(render(draft, project, schema, Some(value), ROOT, "", 0)),
|
||||
Err(error) => out = out.push(text(error).size(11)),
|
||||
}
|
||||
let checked = draft.materialize_scopes(&[ROOT.into()]).and_then(|raw| {
|
||||
let baseline: TraceBaseline =
|
||||
serde_json::from_value(raw["baseline"].clone()).map_err(|e| e.to_string())?;
|
||||
baseline.semantic.as_ref().map_or(Ok(()), |b| {
|
||||
semantic::validate_semantic_baseline(b).map_err(|e| e.join("; "))
|
||||
})
|
||||
});
|
||||
if let Err(error) = checked {
|
||||
out = out.push(text(format!("Stored history is incomplete or invalid: {error}")).size(11));
|
||||
}
|
||||
out.into()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,911 @@
|
||||
use super::*;
|
||||
use crate::document::Document;
|
||||
|
||||
fn fixture(baseline: Option<TraceBaseline>) -> (Project, Id) {
|
||||
let mut project = Project::empty();
|
||||
let oa = project
|
||||
.add_element(Kind::Activity, Layer::Oa, "Assess need", None)
|
||||
.unwrap();
|
||||
let sa = project
|
||||
.add_element(Kind::Function, Layer::Sa, "Assess request", None)
|
||||
.unwrap();
|
||||
let id = Id::from_u128(9001);
|
||||
project.traces.insert(
|
||||
id,
|
||||
Trace {
|
||||
id,
|
||||
alias: "assessment_review".into(),
|
||||
name: "Assessment correspondence".into(),
|
||||
description: "Engineering review history".into(),
|
||||
oa,
|
||||
sa,
|
||||
kind: TraceKind::Refinement,
|
||||
baseline,
|
||||
},
|
||||
);
|
||||
validate_candidate(&project).unwrap();
|
||||
(project, id)
|
||||
}
|
||||
|
||||
fn baseline(facets: Vec<FacetReview>) -> TraceBaseline {
|
||||
TraceBaseline {
|
||||
oa: TracedProperties {
|
||||
name: "Earlier OA name".into(),
|
||||
description: "OA note".into(),
|
||||
},
|
||||
sa: TracedProperties {
|
||||
name: "Earlier SA name".into(),
|
||||
description: "SA note".into(),
|
||||
},
|
||||
semantic: Some(SemanticBaseline { facets }),
|
||||
}
|
||||
}
|
||||
|
||||
fn document(project: &Project) -> Document {
|
||||
let mut bundle = source_project::format_bundle(project);
|
||||
bundle
|
||||
.files
|
||||
.get_mut(&bundle.entry)
|
||||
.unwrap()
|
||||
.insert_str(0, "// Retain this authored project note.\n\n");
|
||||
Document::from_bundle(bundle, Default::default()).unwrap()
|
||||
}
|
||||
|
||||
fn choice(selector: FacetSelector) -> Value {
|
||||
fact_choices()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find(|choice| {
|
||||
serde_json::from_value::<FacetReview>(choice.value.clone())
|
||||
.unwrap()
|
||||
.selector()
|
||||
== selector
|
||||
})
|
||||
.unwrap()
|
||||
.value
|
||||
.clone()
|
||||
}
|
||||
|
||||
fn add_header(draft: &mut Draft) {
|
||||
draft
|
||||
.append(FACETS, choice(FacetSelector::Header(HeaderFacet::Name)))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn assert_history_transaction(document: &mut Document, draft: &Draft, expected: &Project) {
|
||||
let before = document.project().clone();
|
||||
let source_before = document.bundle().clone();
|
||||
document.execute(draft.command().unwrap()).unwrap();
|
||||
assert_eq!(document.project(), expected);
|
||||
let accepted_source = document.bundle().clone();
|
||||
assert!(
|
||||
accepted_source.files[&accepted_source.entry]
|
||||
.starts_with("// Retain this authored project note.\n\n")
|
||||
);
|
||||
assert_eq!(
|
||||
Document::from_bundle(accepted_source.clone(), Default::default())
|
||||
.unwrap()
|
||||
.project(),
|
||||
expected
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_slice::<Project>(&serde_json::to_vec(expected).unwrap()).unwrap(),
|
||||
*expected
|
||||
);
|
||||
assert!(document.undo());
|
||||
assert_eq!(document.project(), &before);
|
||||
assert_eq!(document.bundle(), &source_before);
|
||||
assert!(
|
||||
!document.undo(),
|
||||
"one stored-history Apply must create one history entry"
|
||||
);
|
||||
assert!(document.redo());
|
||||
assert_eq!(document.project(), expected);
|
||||
assert_eq!(document.bundle(), &accepted_source);
|
||||
assert!(!document.redo());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_absent_baseline_and_empty_history_use_explicit_facts_and_exact_document_history() {
|
||||
for initial in [None, Some(baseline(vec![]))] {
|
||||
let (project, id) = fixture(initial);
|
||||
let mut doc = document(&project);
|
||||
let mut draft = Draft::from_project(&project, id).unwrap();
|
||||
assert!(!draft.history.open);
|
||||
draft.shape(TOGGLE, json!(true)).unwrap();
|
||||
assert!(draft.history.open);
|
||||
assert!(
|
||||
!draft.dirty(),
|
||||
"opening the advanced editor cannot acknowledge history"
|
||||
);
|
||||
if draft.value["baseline"].is_null() {
|
||||
draft
|
||||
.shape(ROOT, seed(named("TraceBaseline").unwrap(), 0).unwrap())
|
||||
.unwrap();
|
||||
draft
|
||||
.shape("/baseline/semantic", json!({"facets":[]}))
|
||||
.unwrap();
|
||||
}
|
||||
let untouched = draft.value.clone();
|
||||
assert!(
|
||||
draft
|
||||
.array(FACETS, None, 0)
|
||||
.unwrap_err()
|
||||
.contains("Choose a typed")
|
||||
);
|
||||
assert_eq!(
|
||||
draft.value, untouched,
|
||||
"the old nil-UUID Add must not mutate the draft"
|
||||
);
|
||||
add_header(&mut draft);
|
||||
let before = doc.snapshot();
|
||||
assert!(
|
||||
doc.execute(draft.command().unwrap()).is_err(),
|
||||
"NA/NA is incomplete, not a reviewed default"
|
||||
);
|
||||
assert!(std::sync::Arc::ptr_eq(&before, &doc.snapshot()));
|
||||
let current = current_observation(&draft, &project, &format!("{FACETS}/0")).unwrap();
|
||||
assert_eq!(
|
||||
current,
|
||||
semantic::capture_facet(
|
||||
&project,
|
||||
project.traces[&id].oa,
|
||||
project.traces[&id].sa,
|
||||
&FacetSelector::Header(HeaderFacet::Name)
|
||||
)
|
||||
.unwrap()
|
||||
);
|
||||
draft.shape(&format!("{FACETS}/0"), json!(current)).unwrap();
|
||||
let mut expected = project.clone();
|
||||
expected.traces.get_mut(&id).unwrap().baseline =
|
||||
Some(serde_json::from_value(draft.materialize().unwrap()["baseline"].clone()).unwrap());
|
||||
assert_history_transaction(&mut doc, &draft, &expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_missing_semantic_field_can_be_added_and_removed_without_touching_prose_or_live_objects()
|
||||
{
|
||||
let mut prose = baseline(vec![]);
|
||||
prose.semantic = None;
|
||||
let (project, id) = fixture(Some(prose));
|
||||
let mut doc = document(&project);
|
||||
let mut draft = Draft::from_project(&project, id).unwrap();
|
||||
assert!(draft.value.pointer("/baseline/semantic").is_none());
|
||||
draft
|
||||
.shape("/baseline/semantic", json!({"facets":[]}))
|
||||
.unwrap();
|
||||
let mut expected = project.clone();
|
||||
expected
|
||||
.traces
|
||||
.get_mut(&id)
|
||||
.unwrap()
|
||||
.baseline
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.semantic = Some(SemanticBaseline::default());
|
||||
assert_history_transaction(&mut doc, &draft, &expected);
|
||||
let mut restored = Draft::from_project(&expected, id).unwrap();
|
||||
restored.shape("/baseline/semantic", Value::Null).unwrap();
|
||||
assert_eq!(
|
||||
execute_batch(&expected, &[restored.command().unwrap()]).unwrap(),
|
||||
project
|
||||
);
|
||||
}
|
||||
|
||||
/// Generate type witnesses independently of the editor's deliberately incomplete
|
||||
/// seeds, then ask the real closed Rust types and historical validator to admit them.
|
||||
fn witness(schema: &'static Value, active: &Value, depth: usize) -> Value {
|
||||
assert!(
|
||||
depth <= MAX_DEPTH,
|
||||
"recursive/unsupported historical descriptor"
|
||||
);
|
||||
let schema = resolve(schema).unwrap();
|
||||
if let Some(options) = alternatives(schema) {
|
||||
let selected = if !active.is_null() && active != "NotApplicable" {
|
||||
options.iter().find(|s| matches_value(s, active, 0))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let selected = selected
|
||||
.or_else(|| {
|
||||
options.iter().find(|s| {
|
||||
!matches_value(s, &Value::Null, 0)
|
||||
&& !matches_value(s, &json!("NotApplicable"), 0)
|
||||
})
|
||||
})
|
||||
.unwrap_or(&options[0]);
|
||||
return witness(selected, active, depth + 1);
|
||||
}
|
||||
if let Some(values) = schema.get("enum").and_then(Value::as_array) {
|
||||
return if values.contains(active) {
|
||||
active.clone()
|
||||
} else {
|
||||
values[0].clone()
|
||||
};
|
||||
}
|
||||
match schema["type"].as_str().unwrap() {
|
||||
"object" => schema["properties"]
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|(key, s)| {
|
||||
(
|
||||
key.clone(),
|
||||
witness(s, active.get(key).unwrap_or(&Value::Null), depth + 1),
|
||||
)
|
||||
})
|
||||
.collect::<serde_json::Map<_, _>>()
|
||||
.into(),
|
||||
"array" => json!([witness(&schema["items"], &Value::Null, depth + 1)]),
|
||||
"string" if schema["format"] == "uuid" => json!(Id::from_u128(777)),
|
||||
"string" => json!("Earlier Ω value\nwith provenance"),
|
||||
"integer" => json!(1),
|
||||
"number" => json!(1.25),
|
||||
"boolean" => json!(true),
|
||||
"null" => Value::Null,
|
||||
other => panic!("unsupported historical schema {other}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_every_descriptor_fact_choice_matches_actual_closed_types_and_source_contract() {
|
||||
let (project, id) = fixture(Some(baseline(vec![])));
|
||||
let mut selectors = BTreeSet::new();
|
||||
let choices = fact_choices().unwrap();
|
||||
assert!(
|
||||
choices.len() > 100,
|
||||
"the full scalar and keyed catalog must be reachable"
|
||||
);
|
||||
for entry in choices {
|
||||
let incomplete: FacetReview = serde_json::from_value(entry.value.clone()).unwrap();
|
||||
assert!(
|
||||
selectors.insert(incomplete.selector()),
|
||||
"duplicate Add selector: {}",
|
||||
entry.label
|
||||
);
|
||||
assert!(
|
||||
semantic::validate_semantic_baseline(&SemanticBaseline {
|
||||
facets: vec![incomplete]
|
||||
})
|
||||
.is_err()
|
||||
);
|
||||
let complete = witness(named("FacetReview").unwrap(), &entry.value, 0);
|
||||
let typed: FacetReview = serde_json::from_value(complete.clone())
|
||||
.unwrap_or_else(|e| panic!("{}: {e}", entry.label));
|
||||
semantic::validate_semantic_baseline(&SemanticBaseline {
|
||||
facets: vec![typed.clone()],
|
||||
})
|
||||
.unwrap_or_else(|e| panic!("{}: {e:?}", entry.label));
|
||||
let mut draft = Draft::from_project(&project, id).unwrap();
|
||||
draft.append(FACETS, complete).unwrap();
|
||||
draft.history.open = true;
|
||||
let _controls = view(&draft, &project, &draft.value["baseline"]);
|
||||
let result = execute_batch(&project, &[draft.command().unwrap()]).unwrap();
|
||||
let parsed = source_project::compile_bundle(
|
||||
&source_project::format_bundle(&result),
|
||||
Default::default(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
parsed.decoded.project, result,
|
||||
"source parity for {}",
|
||||
entry.label
|
||||
);
|
||||
assert_eq!(
|
||||
result.traces[&id]
|
||||
.baseline
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.semantic
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.facets,
|
||||
vec![typed]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_deleted_keys_and_observation_uuids_remain_exact_and_duplicate_history_rejects() {
|
||||
let old_link = Id::from_u128(401);
|
||||
let old_owner = Id::from_u128(402);
|
||||
let fact = FacetReview::Artifact {
|
||||
keys: KeyPair {
|
||||
oa: Some(old_link),
|
||||
sa: None,
|
||||
},
|
||||
review: ArtifactReview::Owner(Reviewed {
|
||||
oa: Observed::Value(old_owner),
|
||||
sa: Observed::NotApplicable,
|
||||
}),
|
||||
};
|
||||
let (project, id) = fixture(Some(baseline(vec![fact])));
|
||||
assert!(!project.contains(old_link) && !project.contains(old_owner));
|
||||
let mut doc = document(&project);
|
||||
let mut draft = Draft::from_project(&project, id).unwrap();
|
||||
let key_path = format!("{FACETS}/0/Artifact/keys/oa");
|
||||
let owner_path = format!("{FACETS}/0/Artifact/review/Owner/oa/Value");
|
||||
assert!(!free_string(&key_path) && !free_string(&owner_path));
|
||||
let edited_key = Id::from_u128(403);
|
||||
let edited_owner = Id::from_u128(404);
|
||||
draft.input(&key_path, edited_key.to_string());
|
||||
draft.input(&owner_path, "bad uuid".into());
|
||||
assert!(draft.command().is_err());
|
||||
draft.input(&owner_path, edited_owner.to_string());
|
||||
let accepted = draft
|
||||
.materialize()
|
||||
.unwrap()
|
||||
.pointer(&format!("{FACETS}/0"))
|
||||
.unwrap()
|
||||
.clone();
|
||||
draft.append(FACETS, accepted.clone()).unwrap();
|
||||
let before = doc.snapshot();
|
||||
assert!(
|
||||
doc.execute(draft.command().unwrap()).is_err(),
|
||||
"overlapping historic addresses must reject"
|
||||
);
|
||||
assert!(std::sync::Arc::ptr_eq(&before, &doc.snapshot()));
|
||||
draft.array(FACETS, Some(1), 0).unwrap();
|
||||
let mut expected = project.clone();
|
||||
expected
|
||||
.traces
|
||||
.get_mut(&id)
|
||||
.unwrap()
|
||||
.baseline
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.semantic
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.facets = vec![serde_json::from_value(accepted).unwrap()];
|
||||
assert_history_transaction(&mut doc, &draft, &expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_invalid_numbers_survive_constructor_switch_reorder_removal_and_recovery() {
|
||||
let (project, id) = fixture(Some(baseline(vec![])));
|
||||
let mut draft = Draft::from_project(&project, id).unwrap();
|
||||
draft
|
||||
.append(
|
||||
FACETS,
|
||||
choice(FacetSelector::Data(DataFacet::IntegerMinimum)),
|
||||
)
|
||||
.unwrap();
|
||||
let side = format!("{FACETS}/0/Data/IntegerMinimum/oa");
|
||||
draft
|
||||
.shape(&format!("{VARIANT}{side}"), json!({"Value": 7}))
|
||||
.unwrap();
|
||||
draft.input(&format!("{side}/Value"), "-".into());
|
||||
add_header(&mut draft);
|
||||
assert_eq!(draft.inputs[&format!("{side}/Value")].raw, "-");
|
||||
draft
|
||||
.shape(&format!("{VARIANT}{side}"), json!("NotApplicable"))
|
||||
.unwrap();
|
||||
draft.array(FACETS, Some(0), 1).unwrap();
|
||||
let moved_side = format!("{FACETS}/1/Data/IntegerMinimum/oa");
|
||||
draft
|
||||
.shape(&format!("{VARIANT}{moved_side}"), json!({"Value": 0}))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
draft.inputs[&format!("{moved_side}/Value")].raw,
|
||||
"-",
|
||||
"inactive constructor drafts follow the historical row"
|
||||
);
|
||||
let saved = draft.recovery_snapshot();
|
||||
let mut recovered = Draft::from_recovery(&project, saved).unwrap();
|
||||
assert!(recovered.history.open);
|
||||
assert_eq!(recovered.inputs[&format!("{moved_side}/Value")].raw, "-");
|
||||
assert!(recovered.command().is_err());
|
||||
recovered.array(FACETS, Some(0), 0).unwrap();
|
||||
assert_eq!(recovered.inputs[&format!("{side}/Value")].raw, "-");
|
||||
recovered.input(&format!("{side}/Value"), i64::MIN.to_string());
|
||||
let mut doc = document(&project);
|
||||
let mut expected = project.clone();
|
||||
expected
|
||||
.traces
|
||||
.get_mut(&id)
|
||||
.unwrap()
|
||||
.baseline
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.semantic = Some(SemanticBaseline {
|
||||
facets: vec![FacetReview::Data(DataReview::IntegerMinimum(Reviewed {
|
||||
oa: Observed::Value(Some(i64::MIN)),
|
||||
sa: Observed::NotApplicable,
|
||||
}))],
|
||||
});
|
||||
assert_history_transaction(&mut doc, &recovered, &expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_compound_value_and_constructor_variants_are_editable_without_live_context() {
|
||||
let (project, id) = fixture(Some(baseline(vec![])));
|
||||
let old = Id::from_u128(700);
|
||||
let values = vec![
|
||||
FacetReview::Data(DataReview::Default(Reviewed {
|
||||
oa: Observed::Value(Some(DataValue::Boolean(true))),
|
||||
sa: Observed::NotApplicable,
|
||||
})),
|
||||
FacetReview::Field(FieldReview::Type(Reviewed {
|
||||
oa: Observed::Value(TypeRef::Named(old)),
|
||||
sa: Observed::Value(TypeRef::Real),
|
||||
})),
|
||||
FacetReview::Occurrence(OccurrenceReview::Operation(Reviewed {
|
||||
oa: Observed::NotApplicable,
|
||||
sa: Observed::Value(Some(OperationUse {
|
||||
allocation: Some(old),
|
||||
})),
|
||||
})),
|
||||
FacetReview::Relation(RelationReview::ContextSource(Reviewed {
|
||||
oa: Observed::Value(OccurrenceEndpoint {
|
||||
references: vec![old, Id::from_u128(701)],
|
||||
occurrence: Id::from_u128(702),
|
||||
}),
|
||||
sa: Observed::NotApplicable,
|
||||
})),
|
||||
FacetReview::Relation(RelationReview::ContextPurpose(Reviewed {
|
||||
oa: Observed::Value(ContextualLinkPurpose::CommonWork { work: old }),
|
||||
sa: Observed::NotApplicable,
|
||||
})),
|
||||
FacetReview::Annotation {
|
||||
keys: KeyPair {
|
||||
oa: Some(AnnotationKey {
|
||||
path: vec![old],
|
||||
field: old,
|
||||
}),
|
||||
sa: None,
|
||||
},
|
||||
review: Reviewed {
|
||||
oa: Observed::Value(Some(MessageValue {
|
||||
path: vec![old],
|
||||
field: old,
|
||||
expression: "verbatim\tΩ".into(),
|
||||
})),
|
||||
sa: Observed::NotApplicable,
|
||||
},
|
||||
},
|
||||
FacetReview::Binding {
|
||||
keys: KeyPair {
|
||||
oa: Some(old),
|
||||
sa: None,
|
||||
},
|
||||
review: Reviewed {
|
||||
oa: Observed::Value(Some(LifelineBinding {
|
||||
referenced: old,
|
||||
local: Id::from_u128(702),
|
||||
})),
|
||||
sa: Observed::NotApplicable,
|
||||
},
|
||||
},
|
||||
];
|
||||
let mut draft = Draft::from_project(&project, id).unwrap();
|
||||
for value in &values {
|
||||
draft.append(FACETS, json!(value)).unwrap();
|
||||
}
|
||||
let literal = format!("{FACETS}/0/Data/Default/oa/Value");
|
||||
for value in [
|
||||
DataValue::Integer(i64::MAX),
|
||||
DataValue::Real(-0.25),
|
||||
DataValue::String("quoted\r\nΩ".into()),
|
||||
DataValue::Enumeration("Literal".into()),
|
||||
] {
|
||||
draft
|
||||
.shape(&format!("{VARIANT}{literal}"), json!(value))
|
||||
.unwrap();
|
||||
let model = execute_batch(&project, &[draft.command().unwrap()]).unwrap();
|
||||
assert!(
|
||||
matches!(&model.traces[&id].baseline.as_ref().unwrap().semantic.as_ref().unwrap().facets[0], FacetReview::Data(DataReview::Default(Reviewed { oa: Observed::Value(Some(actual)), .. })) if actual == &value)
|
||||
);
|
||||
}
|
||||
let purpose = format!("{FACETS}/4/Relation/ContextPurpose/oa/Value");
|
||||
draft
|
||||
.shape(
|
||||
&format!("{VARIANT}{purpose}"),
|
||||
json!({"Precedence":{"guard":"reviewed before dispatch"}}),
|
||||
)
|
||||
.unwrap();
|
||||
let mut doc = document(&project);
|
||||
let mut expected = project.clone();
|
||||
expected.traces.get_mut(&id).unwrap().baseline =
|
||||
Some(serde_json::from_value(draft.materialize().unwrap()["baseline"].clone()).unwrap());
|
||||
assert_history_transaction(&mut doc, &draft, &expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_exact_strings_do_not_confuse_uuid_shaped_prose_or_literal_keys_with_references() {
|
||||
let (project, id) = fixture(Some(baseline(vec![])));
|
||||
let mut draft = Draft::from_project(&project, id).unwrap();
|
||||
add_header(&mut draft);
|
||||
let side = format!("{FACETS}/0/Header/Name/oa");
|
||||
draft
|
||||
.shape(&format!("{VARIANT}{side}"), json!({"Value":""}))
|
||||
.unwrap();
|
||||
let path = format!("{side}/Value");
|
||||
assert!(free_string(&path));
|
||||
draft.set_exact(true).unwrap();
|
||||
let value = "00000000-0000-0000-0000-000000000077\tΩ\r\nname";
|
||||
draft.input(&path, serde_json::to_string(value).unwrap());
|
||||
draft
|
||||
.append(
|
||||
FACETS,
|
||||
choice(FacetSelector::Literal(KeyPair { oa: None, sa: None })),
|
||||
)
|
||||
.unwrap();
|
||||
let key = format!("{FACETS}/1/Literal/keys/oa");
|
||||
draft
|
||||
.shape(&format!("{VARIANT}{key}"), json!("literal"))
|
||||
.unwrap();
|
||||
draft.input(&key, serde_json::to_string("literal\tΩ").unwrap());
|
||||
draft
|
||||
.shape(
|
||||
&format!("{VARIANT}{FACETS}/1/Literal/review/oa"),
|
||||
json!({"Value":true}),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(free_string(&key));
|
||||
let mut expected = project.clone();
|
||||
expected
|
||||
.traces
|
||||
.get_mut(&id)
|
||||
.unwrap()
|
||||
.baseline
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.semantic = Some(SemanticBaseline {
|
||||
facets: vec![
|
||||
FacetReview::Header(HeaderReview::Name(Reviewed {
|
||||
oa: Observed::Value(value.into()),
|
||||
sa: Observed::NotApplicable,
|
||||
})),
|
||||
FacetReview::Literal {
|
||||
keys: KeyPair {
|
||||
oa: Some("literal\tΩ".into()),
|
||||
sa: None,
|
||||
},
|
||||
review: Reviewed {
|
||||
oa: Observed::Value(true),
|
||||
sa: Observed::NotApplicable,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
assert_history_transaction(&mut document(&project), &draft, &expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_primitive_type_choices_remain_visible_inside_the_tagged_union() {
|
||||
let schema = resolve(named("TypeRef").unwrap()).unwrap();
|
||||
let branches = alternatives(schema).unwrap();
|
||||
let primitives = branches
|
||||
.iter()
|
||||
.find(|branch| {
|
||||
resolve(branch)
|
||||
.unwrap()
|
||||
.get("enum")
|
||||
.and_then(Value::as_array)
|
||||
.is_some_and(|values| values.len() > 1)
|
||||
})
|
||||
.unwrap();
|
||||
assert!(
|
||||
branch_has_fields(primitives),
|
||||
"the primitive branch needs an inner picker, not only its outer union label"
|
||||
);
|
||||
let (project, id) = fixture(Some(baseline(vec![])));
|
||||
let mut draft = Draft::from_project(&project, id).unwrap();
|
||||
draft
|
||||
.append(
|
||||
FACETS,
|
||||
json!(FacetReview::Field(FieldReview::Type(Reviewed {
|
||||
oa: Observed::Value(TypeRef::Named(Id::from_u128(888))),
|
||||
sa: Observed::NotApplicable,
|
||||
}))),
|
||||
)
|
||||
.unwrap();
|
||||
let path = format!("{FACETS}/0/Field/Type/oa/Value");
|
||||
draft
|
||||
.shape(
|
||||
&format!("{VARIANT}{path}"),
|
||||
branch_seed(primitives).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
for value in [
|
||||
TypeRef::Boolean,
|
||||
TypeRef::Integer,
|
||||
TypeRef::Real,
|
||||
TypeRef::String,
|
||||
] {
|
||||
assert!(matches_value(primitives, &json!(value), 0));
|
||||
draft.shape(&path, json!(value)).unwrap();
|
||||
let mut expected = project.clone();
|
||||
expected
|
||||
.traces
|
||||
.get_mut(&id)
|
||||
.unwrap()
|
||||
.baseline
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.semantic = Some(SemanticBaseline {
|
||||
facets: vec![FacetReview::Field(FieldReview::Type(Reviewed {
|
||||
oa: Observed::Value(value),
|
||||
sa: Observed::NotApplicable,
|
||||
}))],
|
||||
});
|
||||
assert_history_transaction(&mut document(&project), &draft, &expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_retained_text_constructors_follow_exact_mode_without_changing_the_observation() {
|
||||
let (project, id) = fixture(Some(baseline(vec![])));
|
||||
let mut draft = Draft::from_project(&project, id).unwrap();
|
||||
add_header(&mut draft);
|
||||
let side = format!("{FACETS}/0/Header/Name/oa");
|
||||
let path = format!("{side}/Value");
|
||||
let stored = "Earlier \"quoted\" name\tΩ\r\nsecond line";
|
||||
draft
|
||||
.shape(&format!("{VARIANT}{side}"), json!({"Value":""}))
|
||||
.unwrap();
|
||||
draft.input(&path, stored.into());
|
||||
draft
|
||||
.shape(&format!("{VARIANT}{side}"), json!("NotApplicable"))
|
||||
.unwrap();
|
||||
draft.set_exact(true).unwrap();
|
||||
draft
|
||||
.shape(&format!("{VARIANT}{side}"), json!({"Value":""}))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
draft.inputs[&path].raw,
|
||||
serde_json::to_string(stored).unwrap()
|
||||
);
|
||||
draft
|
||||
.shape(&format!("{VARIANT}{side}"), json!("NotApplicable"))
|
||||
.unwrap();
|
||||
draft.set_exact(false).unwrap();
|
||||
draft
|
||||
.shape(&format!("{VARIANT}{side}"), json!({"Value":""}))
|
||||
.unwrap();
|
||||
assert_eq!(draft.inputs[&path].raw, stored);
|
||||
let mut expected = project.clone();
|
||||
expected
|
||||
.traces
|
||||
.get_mut(&id)
|
||||
.unwrap()
|
||||
.baseline
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.semantic = Some(SemanticBaseline {
|
||||
facets: vec![FacetReview::Header(HeaderReview::Name(Reviewed {
|
||||
oa: Observed::Value(stored.into()),
|
||||
sa: Observed::NotApplicable,
|
||||
}))],
|
||||
});
|
||||
assert_history_transaction(&mut document(&project), &draft, &expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_clear_discards_inactive_invalid_constructors_before_reusing_the_same_row() {
|
||||
let (project, id) = fixture(Some(baseline(vec![])));
|
||||
let mut draft = Draft::from_project(&project, id).unwrap();
|
||||
add_header(&mut draft);
|
||||
let side = format!("{FACETS}/0/Header/Name/oa");
|
||||
let path = format!("{side}/Value");
|
||||
draft
|
||||
.shape(&format!("{VARIANT}{side}"), json!({"Value":""}))
|
||||
.unwrap();
|
||||
draft.set_exact(true).unwrap();
|
||||
draft.input(&path, "\"unfinished".into());
|
||||
draft
|
||||
.shape(&format!("{VARIANT}{side}"), json!("NotApplicable"))
|
||||
.unwrap();
|
||||
assert!(
|
||||
draft.set_exact(false).is_err(),
|
||||
"invalid retained quoted text cannot silently change modes"
|
||||
);
|
||||
assert!(draft.exact_strings);
|
||||
draft.shape(ROOT, Value::Null).unwrap();
|
||||
assert!(draft.constructor_drafts.is_empty());
|
||||
draft.set_exact(false).unwrap();
|
||||
draft.shape(ROOT, json!(baseline(vec![]))).unwrap();
|
||||
add_header(&mut draft);
|
||||
draft
|
||||
.shape(
|
||||
&format!("{VARIANT}{side}"),
|
||||
json!({"Value":"Fresh observation"}),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(draft.inputs[&path].raw, "Fresh observation");
|
||||
let mut expected = project.clone();
|
||||
expected
|
||||
.traces
|
||||
.get_mut(&id)
|
||||
.unwrap()
|
||||
.baseline
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.semantic = Some(SemanticBaseline {
|
||||
facets: vec![FacetReview::Header(HeaderReview::Name(Reviewed {
|
||||
oa: Observed::Value("Fresh observation".into()),
|
||||
sa: Observed::NotApplicable,
|
||||
}))],
|
||||
});
|
||||
assert_history_transaction(&mut document(&project), &draft, &expected);
|
||||
}
|
||||
|
||||
/// Exercise the same descriptor choice and FieldShape event emitted by the
|
||||
/// nested picker. Injecting an already complete Observed value skips this path.
|
||||
fn choose_history_constructor(draft: &mut Draft, path: &str, label: &str) {
|
||||
let schema = resolve(schema_at(path).expect("typed history field")).unwrap();
|
||||
let branch = alternatives(schema)
|
||||
.expect("constructor choices")
|
||||
.iter()
|
||||
.find(|branch| branch_label(branch) == label)
|
||||
.expect("visible constructor choice");
|
||||
let event = variant_message(
|
||||
path,
|
||||
branch_seed(branch).unwrap(),
|
||||
draft.value.pointer(path).is_some(),
|
||||
);
|
||||
let Message::FieldShape(path, value) = event else {
|
||||
panic!("constructor picker must emit FieldShape");
|
||||
};
|
||||
draft.shape(&path, value).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_nested_optional_number_events_switch_absent_present_and_recover_invalid_raw() {
|
||||
let (project, id) = fixture(Some(baseline(vec![])));
|
||||
let mut draft = Draft::from_project(&project, id).unwrap();
|
||||
draft
|
||||
.append(
|
||||
FACETS,
|
||||
choice(FacetSelector::Data(DataFacet::IntegerMinimum)),
|
||||
)
|
||||
.unwrap();
|
||||
let side = format!("{FACETS}/0/Data/IntegerMinimum/oa");
|
||||
let path = format!("{side}/Value");
|
||||
choose_history_constructor(&mut draft, &side, "Observed value");
|
||||
assert_eq!(draft.value.pointer(&path), Some(&Value::Null));
|
||||
choose_history_constructor(&mut draft, &path, "Set number");
|
||||
assert_eq!(
|
||||
draft.value.pointer(&path),
|
||||
Some(&json!(0)),
|
||||
"null and numeric constructor identities must differ"
|
||||
);
|
||||
draft.input(&path, "-".into());
|
||||
choose_history_constructor(&mut draft, &path, "Absent (none)");
|
||||
assert_eq!(draft.value.pointer(&path), Some(&Value::Null));
|
||||
let mut draft = Draft::from_recovery(&project, draft.recovery_snapshot()).unwrap();
|
||||
choose_history_constructor(&mut draft, &path, "Set number");
|
||||
assert_eq!(draft.inputs[&path].raw, "-");
|
||||
assert!(draft.command().is_err());
|
||||
draft.input(&path, "7".into());
|
||||
let mut expected = project.clone();
|
||||
expected
|
||||
.traces
|
||||
.get_mut(&id)
|
||||
.unwrap()
|
||||
.baseline
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.semantic = Some(SemanticBaseline {
|
||||
facets: vec![FacetReview::Data(DataReview::IntegerMinimum(Reviewed {
|
||||
oa: Observed::Value(Some(7)),
|
||||
sa: Observed::NotApplicable,
|
||||
}))],
|
||||
});
|
||||
assert_history_transaction(&mut document(&project), &draft, &expected);
|
||||
choose_history_constructor(&mut draft, &path, "Absent (none)");
|
||||
expected
|
||||
.traces
|
||||
.get_mut(&id)
|
||||
.unwrap()
|
||||
.baseline
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.semantic = Some(SemanticBaseline {
|
||||
facets: vec![FacetReview::Data(DataReview::IntegerMinimum(Reviewed {
|
||||
oa: Observed::Value(None),
|
||||
sa: Observed::NotApplicable,
|
||||
}))],
|
||||
});
|
||||
assert_history_transaction(&mut document(&project), &draft, &expected);
|
||||
choose_history_constructor(&mut draft, &path, "Set number");
|
||||
assert_eq!(
|
||||
draft.inputs[&path].raw, "7",
|
||||
"returning to Value restores its edited contents"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_nested_uuid_and_list_events_preserve_present_values_and_tagged_item_branches() {
|
||||
let (project, id) = fixture(Some(baseline(vec![])));
|
||||
let mut draft = Draft::from_project(&project, id).unwrap();
|
||||
draft
|
||||
.append(FACETS, choice(FacetSelector::Header(HeaderFacet::Parent)))
|
||||
.unwrap();
|
||||
draft
|
||||
.append(FACETS, choice(FacetSelector::Field(FieldFacet::Default)))
|
||||
.unwrap();
|
||||
let side = format!("{FACETS}/0/Header/Parent/oa");
|
||||
let uuid = format!("{side}/Value");
|
||||
choose_history_constructor(&mut draft, &side, "Observed value");
|
||||
choose_history_constructor(&mut draft, &uuid, "Set historical UUID");
|
||||
assert_eq!(
|
||||
draft.value.pointer(&uuid),
|
||||
Some(&json!("")),
|
||||
"empty UUID input is distinct from absent"
|
||||
);
|
||||
assert!(draft.command().is_err());
|
||||
draft.input(&uuid, "incomplete-uuid".into());
|
||||
choose_history_constructor(&mut draft, &uuid, "Absent (none)");
|
||||
choose_history_constructor(&mut draft, &uuid, "Set historical UUID");
|
||||
assert_eq!(draft.inputs[&uuid].raw, "incomplete-uuid");
|
||||
let historical = Id::from_u128(8765);
|
||||
draft.input(&uuid, historical.to_string());
|
||||
choose_history_constructor(&mut draft, &uuid, "Absent (none)");
|
||||
choose_history_constructor(&mut draft, &uuid, "Set historical UUID");
|
||||
assert_eq!(draft.inputs[&uuid].raw, historical.to_string());
|
||||
|
||||
let side = format!("{FACETS}/1/Field/Default/oa");
|
||||
let list = format!("{side}/Value");
|
||||
choose_history_constructor(&mut draft, &side, "Observed value");
|
||||
choose_history_constructor(&mut draft, &list, "Set list");
|
||||
assert_eq!(
|
||||
draft.value.pointer(&list),
|
||||
Some(&json!([])),
|
||||
"empty list is distinct from absent"
|
||||
);
|
||||
let array_schema = alternatives(resolve(schema_at(&list).unwrap()).unwrap())
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|branch| resolve(branch).unwrap())
|
||||
.find(|branch| branch["type"] == "array")
|
||||
.unwrap();
|
||||
let event = Message::FieldAppend(list.clone(), seed(&array_schema["items"], 0).unwrap());
|
||||
let Message::FieldAppend(path, value) = event else {
|
||||
unreachable!()
|
||||
};
|
||||
draft.append(&path, value).unwrap();
|
||||
let item = format!("{list}/0");
|
||||
choose_history_constructor(&mut draft, &item, "Integer");
|
||||
draft.input(&format!("{item}/Integer"), "-".into());
|
||||
choose_history_constructor(&mut draft, &item, "String");
|
||||
draft.input(&format!("{item}/String"), "retained textual branch".into());
|
||||
choose_history_constructor(&mut draft, &item, "Integer");
|
||||
assert_eq!(draft.inputs[&format!("{item}/Integer")].raw, "-");
|
||||
draft.input(&format!("{item}/Integer"), "21".into());
|
||||
choose_history_constructor(&mut draft, &list, "Absent (none)");
|
||||
assert_eq!(draft.value.pointer(&list), Some(&Value::Null));
|
||||
let mut draft = Draft::from_recovery(&project, draft.recovery_snapshot()).unwrap();
|
||||
choose_history_constructor(&mut draft, &list, "Set list");
|
||||
assert_eq!(draft.inputs[&format!("{item}/Integer")].raw, "21");
|
||||
choose_history_constructor(&mut draft, &item, "String");
|
||||
assert_eq!(
|
||||
draft.inputs[&format!("{item}/String")].raw,
|
||||
"retained textual branch"
|
||||
);
|
||||
let mut expected = project.clone();
|
||||
expected
|
||||
.traces
|
||||
.get_mut(&id)
|
||||
.unwrap()
|
||||
.baseline
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.semantic = Some(SemanticBaseline {
|
||||
facets: vec![
|
||||
FacetReview::Header(HeaderReview::Parent(Reviewed {
|
||||
oa: Observed::Value(Some(historical)),
|
||||
sa: Observed::NotApplicable,
|
||||
})),
|
||||
FacetReview::Field(FieldReview::Default(Reviewed {
|
||||
oa: Observed::Value(Some(vec![DataValue::String(
|
||||
"retained textual branch".into(),
|
||||
)])),
|
||||
sa: Observed::NotApplicable,
|
||||
})),
|
||||
],
|
||||
});
|
||||
assert_history_transaction(&mut document(&project), &draft, &expected);
|
||||
}
|
||||
@@ -655,7 +655,7 @@ fn article(topic: Topic) -> Article {
|
||||
),
|
||||
explain(
|
||||
"Review drift deliberately",
|
||||
"On an existing trace, choose Review counterpart changes… to compare typed content and prior observations. Choose Keep for the destination, Take for the source, or Set final value / Set final kind and payload where offered. Review mapped references and proposed changes, then choose Apply reviewed changes. Independent edits may be intentional; keeping a value records that decision. A manual trace records correspondence without propagating properties.",
|
||||
"On an existing trace, choose Review counterpart changes… to compare typed content and prior observations. Choose Keep for the destination, Take for the source, or Set final value / Set final kind and payload where offered. Review mapped references and proposed changes, then choose Apply reviewed changes. Independent edits may be intentional; keeping a value records that decision. Manual traces record engineer-defined correspondence. Ordinary edits never synchronize counterparts automatically; an explicit supported transition or review can update selected properties, including for a Manual trace.",
|
||||
),
|
||||
explain(
|
||||
"Know what reconciliation changes",
|
||||
|
||||
+21
-11
@@ -667,7 +667,7 @@ Actual verification evidence is separate from this contract.</p>
|
||||
</section>
|
||||
<section id="one-authoritative-model">
|
||||
<h2>2. One authoritative model</h2>
|
||||
<p>A Project stores <span class="docutils literal">schema_version</span> (6), <span class="docutils literal">id</span>, <span class="docutils literal">alias</span>, <span class="docutils literal">name</span>,
|
||||
<p>A Project stores <span class="docutils literal">schema_version</span> (7), <span class="docutils literal">id</span>, <span class="docutils literal">alias</span>, <span class="docutils literal">name</span>,
|
||||
<span class="docutils literal">description</span>, and maps named <span class="docutils literal">elements</span>, <span class="docutils literal">relations</span>, <span class="docutils literal">diagrams</span>,
|
||||
<span class="docutils literal">artifact_links</span>, and <span class="docutils literal">traces</span>. Each map is keyed by the UUID contained in
|
||||
its record. Duplicate map keys and mismatching keys/identities are errors.</p>
|
||||
@@ -730,6 +730,10 @@ lists all additional fields. <span class="docutils literal">ref</span> means <sp
|
||||
<td><p><span class="docutils literal">kind</span>: Information, Material, Energy; <span class="docutils literal">mechanism</span>: ExchangeMechanism; <span class="docutils literal">supertype</span>: item ref?</p></td>
|
||||
<td><p>Information; Unspecified; none</p></td>
|
||||
</tr>
|
||||
<tr><td><p><span class="docutils literal">oa_exchange_category</span>, <span class="docutils literal">sa_exchange_category</span></p></td>
|
||||
<td><p><span class="docutils literal">family</span>: Work or Participant; <span class="docutils literal">exchanges</span>: same-layer exchange refs</p></td>
|
||||
<td><p>Work; <span class="docutils literal">[]</span>; root-level only</p></td>
|
||||
</tr>
|
||||
<tr><td><p><span class="docutils literal">oa_data_type</span>, <span class="docutils literal">sa_data_type</span></p></td>
|
||||
<td><p><span class="docutils literal">definition</span>: DataTypeDefinition; <span class="docutils literal">default</span>: DataValue?; <span class="docutils literal">unit</span>: string</p></td>
|
||||
<td><p>String with minimum_length 0 / maximum_length none; none; <span class="docutils literal">""</span></p></td>
|
||||
@@ -869,7 +873,9 @@ work and produces a warning. The System cannot own internal components.
|
||||
The present Studio profile does not decompose SA Actors.</p>
|
||||
<p>All relations have a header identity, alias, display name, optional
|
||||
<span class="docutils literal">description</span> (default <span class="docutils literal">""</span>), and required <span class="docutils literal">source</span> and <span class="docutils literal">target</span>
|
||||
references. Their kind is the declaration keyword:</p>
|
||||
endpoints. Ordinary endpoints are semantic references; ContextualLink endpoints
|
||||
are qualified OccurrenceEndpoint records resolved from its required <span class="docutils literal">owner</span>.
|
||||
Their kind is the declaration keyword:</p>
|
||||
<table>
|
||||
<caption>Relation catalog</caption>
|
||||
<colgroup>
|
||||
@@ -956,6 +962,10 @@ references. Their kind is the declaration keyword:</p>
|
||||
<td><p>Preceding contribution → following contribution in one process/chain</p></td>
|
||||
<td><p><span class="docutils literal">guard</span> string / ""</p></td>
|
||||
</tr>
|
||||
<tr><td><p><span class="docutils literal">contextual_link</span></p></td>
|
||||
<td><p>Qualified occurrence endpoints resolved from an explicit owner Process/FunctionalChain; section 4.2.1</p></td>
|
||||
<td><p>Required <span class="docutils literal">owner</span>, <span class="docutils literal">source</span> and <span class="docutils literal">target</span> OccurrenceEndpoint records, and typed <span class="docutils literal">purpose</span>: Precedence, Exchange or CommonWork</p></td>
|
||||
</tr>
|
||||
<tr><td><p><span class="docutils literal">state_transition</span></p></td>
|
||||
<td><p>State vertices in one machine, subject to section 4.4</p></td>
|
||||
<td><p><span class="docutils literal">trigger</span>, <span class="docutils literal">guard</span>, <span class="docutils literal">effect</span> strings / ""; <span class="docutils literal">transition_kind</span> External; <span class="docutils literal">triggers</span>, <span class="docutils literal">effects</span> reference lists / []</p></td>
|
||||
@@ -1811,7 +1821,7 @@ are invalid.</p>
|
||||
<td><p>Operational Mission → System Mission</p></td>
|
||||
</tr>
|
||||
<tr><td><p>Exchange</p></td>
|
||||
<td><p>Interaction relation → FunctionalExchange relation</p></td>
|
||||
<td><p>Interaction → FunctionalExchange, or CommunicationMean → BoundaryExchange</p></td>
|
||||
</tr>
|
||||
<tr><td><p>Sequence</p></td>
|
||||
<td><p>Process → FunctionalChain, or paired occurrence contributions directly owned by Process / FunctionalChain</p></td>
|
||||
@@ -2261,7 +2271,7 @@ deeply nested drafts produce a diagnostic instead of exhausting the parser stack
|
||||
<h3>6.1. Modular source projects</h3>
|
||||
<p>The pure <span class="docutils literal">source_project</span> core API compiles a virtual source bundle, containing
|
||||
<span class="docutils literal">entry</span> and a <span class="docutils literal">files</span> map from logical paths to UTF-8 strings. Its entry is a
|
||||
root-level <span class="docutils literal">.arcadia</span> file with the source5 project header above. A project entry
|
||||
root-level <span class="docutils literal">.arcadia</span> file with the source7 project header above. A project entry
|
||||
MAY also be <span class="docutils literal">include string ";"</span>. An included fragment consists only of ordinary
|
||||
declarations and include statements; it MUST NOT contain another version header,
|
||||
project wrapper, or project description. Includes MUST occur at document level,
|
||||
@@ -2324,15 +2334,15 @@ the source index. Columns remain one-based Unicode scalars within the real file;
|
||||
spans never refer to concatenated or synthetic source.</p>
|
||||
<p>Single-string parse/lint APIs remain filesystem-free and reject include
|
||||
statements with an explicit source-bundle requirement. V1/V2 single-file imports
|
||||
continue through explicit migration. Modular projects accept source3, source4 and
|
||||
source5. Source3 is upgraded as one project, preserving file paths, includes, nested
|
||||
continue through explicit migration. Modular projects accept source3 through
|
||||
source7. Older bundles are upgraded as one project, preserving file paths, includes, nested
|
||||
ownership and comments through targeted token edits. Generated allocation
|
||||
declarations stay in their interface's existing file. Individual include fragments
|
||||
are not independently migrated. Missing UUIDs stabilize before migration so new
|
||||
allocation identities are deterministic on subsequent compilation.
|
||||
An authored source4 bundle upgrades through the strict prose-only baseline
|
||||
adapter; after any missing UUID stabilization, the version token alone changes
|
||||
to 5. Existing prose, comments, includes and nested ownership remain intact.
|
||||
to 7. Existing prose, comments, includes and nested ownership remain intact.
|
||||
Source5 semantic facet comments follow their closed selector identity through
|
||||
row reordering and reidentification; UUID-looking text does not act as an Id.</p>
|
||||
</section>
|
||||
@@ -2654,7 +2664,7 @@ defaults without changing any prior identity, name, reference, order, geometry,
|
||||
artifact or trace field. The original V2 golden source/JSON in
|
||||
<span class="docutils literal"><span class="pre">crates/oa-core/tests/fixtures</span></span> MUST NOT be regenerated from the newer demo;
|
||||
they verify that every old persistent field survives. Canonical output writes
|
||||
the added defaults through the explicit schema-3, schema-4 and schema-5 migrations.</p>
|
||||
the added defaults through explicit migration to the current schema 7.</p>
|
||||
<p>The V2 adapter preserves every original identity, alias, prose, parent,
|
||||
placement, trace and artifact. Generic ports become Legacy with their original
|
||||
direction/interface record. Existing FE endpoint IDs remain unchanged. For
|
||||
@@ -2671,13 +2681,13 @@ IDs, required resolutions, generated endpoint IDs/aliases/owners, and boundary
|
||||
orientation defaults. REST import, replacement, candidate validation and
|
||||
project snapshots surface the same report. Opening an old server envelope
|
||||
preserves its resource ID/revision and bytes; the next explicit edit writes
|
||||
schema 5. Model/source exports contain only the canonical model. Migration
|
||||
schema 7. Model/source exports contain only the canonical model. Migration
|
||||
reports are transient response metadata, not persisted model facts.</p>
|
||||
<p>Admission checks account for generated objects and their references before
|
||||
construction. Both the old V2 contract and the resulting V5 graph are
|
||||
construction. Both the old V2 contract and the resulting current graph are
|
||||
validated. Migration itself does not save or increase a stored revision.
|
||||
Canonical second saves are stable. V1 normalization produces the frozen V2
|
||||
model and then uses these same V2-to-3-to-4-to-5 stages.</p>
|
||||
model and then uses the same explicit adapters to reach schema 7.</p>
|
||||
<p>Save validates a snapshot, writes a sibling temporary file, flushes it, and
|
||||
atomically replaces the destination. Failed serialization or validation must
|
||||
not truncate a previous file. External concurrent-writer conflict resolution
|
||||
|
||||
+10
-2
@@ -103,6 +103,9 @@ lists all additional fields. ``ref`` means ``@alias``; ``ref?`` also accepts
|
||||
* - ``oa_exchange_item``, ``sa_exchange_item``
|
||||
- ``kind``: Information, Material, Energy; ``mechanism``: ExchangeMechanism; ``supertype``: item ref?
|
||||
- Information; Unspecified; none
|
||||
* - ``oa_exchange_category``, ``sa_exchange_category``
|
||||
- ``family``: Work or Participant; ``exchanges``: same-layer exchange refs
|
||||
- Work; ``[]``; root-level only
|
||||
* - ``oa_data_type``, ``sa_data_type``
|
||||
- ``definition``: DataTypeDefinition; ``default``: DataValue?; ``unit``: string
|
||||
- String with minimum_length 0 / maximum_length none; none; ``""``
|
||||
@@ -223,7 +226,9 @@ The present Studio profile does not decompose SA Actors.
|
||||
|
||||
All relations have a header identity, alias, display name, optional
|
||||
``description`` (default ``""``), and required ``source`` and ``target``
|
||||
references. Their kind is the declaration keyword:
|
||||
endpoints. Ordinary endpoints are semantic references; ContextualLink endpoints
|
||||
are qualified OccurrenceEndpoint records resolved from its required ``owner``.
|
||||
Their kind is the declaration keyword:
|
||||
|
||||
.. list-table:: Relation catalog
|
||||
:header-rows: 1
|
||||
@@ -286,6 +291,9 @@ references. Their kind is the declaration keyword:
|
||||
* - ``sequence_link``
|
||||
- Preceding contribution → following contribution in one process/chain
|
||||
- ``guard`` string / ""
|
||||
* - ``contextual_link``
|
||||
- Qualified occurrence endpoints resolved from an explicit owner Process/FunctionalChain; section 4.2.1
|
||||
- Required ``owner``, ``source`` and ``target`` OccurrenceEndpoint records, and typed ``purpose``: Precedence, Exchange or CommonWork
|
||||
* - ``state_transition``
|
||||
- State vertices in one machine, subject to section 4.4
|
||||
- ``trigger``, ``guard``, ``effect`` strings / ""; ``transition_kind`` External; ``triggers``, ``effects`` reference lists / []
|
||||
@@ -1190,7 +1198,7 @@ are invalid.
|
||||
* - Mission
|
||||
- Operational Mission → System Mission
|
||||
* - Exchange
|
||||
- Interaction relation → FunctionalExchange relation
|
||||
- Interaction → FunctionalExchange, or CommunicationMean → BoundaryExchange
|
||||
* - Sequence
|
||||
- Process → FunctionalChain, or paired occurrence contributions directly owned by Process / FunctionalChain
|
||||
* - Item
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ Arcadia Studio implements a systems-engineering workbench spanning Operational A
|
||||
| States and conditions | Participant-owned state machines, nested regions, states/modes and typed pseudostates, signal/time/change events, trigger/guard/effect transitions, linked entry/do/exit work, availability and constraints |
|
||||
| Perspectives and evidence | Bidirectional counterpart creation/linking, reviewed descriptive and typed semantic changes, explicit ordered collections and combined multi-source review, many-to-many typed/manual traces, arbitrary artifact URI/revision/fragment references |
|
||||
| Views and editing | Independent and duplicated diagrams, decomposition/allocation nesting, collapse, pan/zoom/Fit/Arrange, editable connection routes and labels, four-side port positioning, direct interaction/communication/functional/boundary exchange tools, named exchange categories, saved line-kind/category/coverage/context/hide/label settings, contextual menus, SVG export, model explorer and typed properties |
|
||||
| Text and storage | `arcadia 6` source projects with relative includes and nested declarations, stable aliases/UUIDs, a source-file tree, per-file drafts and diagnostics, comment-preserving graphical updates, exact model/source Undo/Redo, legacy migration, per-file atomic replacement with interrupted source-save recovery, Home/recents and private unsaved-work checkpoints |
|
||||
| Text and storage | `arcadia 7` source projects with relative includes and nested declarations, stable aliases/UUIDs, a source-file tree, per-file drafts and diagnostics, comment-preserving graphical updates, exact model/source Undo/Redo, legacy migration, per-file atomic replacement with interrupted source-save recovery, Home/recents and private unsaved-work checkpoints |
|
||||
| Automation and hosted editing | Rust REST service for project/source-bundle/query/validation/transaction/transition/artifact workflows; optional desktop catalog/open/publish, explicit conditional Save, server review and local Save as; ETag preconditions, bounded requests and atomic current-revision storage |
|
||||
|
||||
Graphical and textual parity applies to the persistent fields of this profile. Diagram placements are model data; camera, selection, source cursor and undo history are session state. Canvas gestures need not match textual operations one for one: typed inspectors and explicit commands expose precise semantics. Full source replacement must repair dependencies in its candidate, while graphical delete/reidentify commands maintain references automatically.
|
||||
|
||||
@@ -211,6 +211,57 @@ Decision: retain same-layer admission for ordinary diagrams, but allow both-laye
|
||||
|
||||
Alternatives rejected: filtering every view by its labelled layer loses existing Traceability content; ignoring layer validation everywhere hides mistaken references; serializing temporary GUI filters breaks source/API parity. Revisit if native workflows or cross-layer regression evidence exposes ambiguity. Current implementation is pending integrated tests and native acceptance, not an established release result.
|
||||
|
||||
## Continuous behavior authoring — 2026-09-10
|
||||
|
||||
Problem: adding the first Exchange to an empty process changed its interactive
|
||||
Graph into an ordered sequence Scene without gesture tools. Scenario non-message
|
||||
creation discarded the visible operand and insertion point, defaulting to root
|
||||
append. These are verified source paths; new-binary native acceptance is pending.
|
||||
|
||||
Select one interactive process renderer. Project legacy ordered adjacency as
|
||||
endpoint pairs without fabricated semantic relation IDs. Loading and selecting
|
||||
remain read-only. The first topology mutation uses the existing validated batch
|
||||
to materialize unaffected adjacency and splice the chosen insertion atomically.
|
||||
Reject the alternative of a special 0/1-element renderer exception, which would
|
||||
reintroduce the discontinuity at a later element count; also reject automatic
|
||||
load-time migration and artificial self-links. Core legacy/explicit semantics
|
||||
and the source version are unchanged. Revisit if actual imported-path editing
|
||||
cannot preserve ordering, rejection and exact source/Undo behavior.
|
||||
|
||||
Scenario Action, State, Reuse and empty Fragment creation capture the clicked
|
||||
owner, operand, before-occurrence and optional lifeline before starting the
|
||||
existing typed draft. Keep current validation, recovery and atomic history.
|
||||
Fragment range wrapping and cross-operand movement require separate explicit
|
||||
transactions. Do not infer persisted lifeline geometry from a click: the current
|
||||
model does not store horizontal lifeline order. Reused-scenario navigation uses
|
||||
bounded transient caller/use context, never an extra model history entry.
|
||||
|
||||
Evidence and acceptance: `release-behavior-audit-20260910.md`, Gitea #1, new
|
||||
production-pointer regression paths and independent source reviews. Compilation,
|
||||
automated execution and native validation are still pending at this decision.
|
||||
|
||||
## Typed stored observations — 2026-09-10
|
||||
|
||||
Problem: generic trace-baseline Add inserted a UUID string into the closed
|
||||
FacetReview list, producing an invalid draft. Hiding Add and routing only to
|
||||
current review/reset cannot fulfill the normative contract: historical values
|
||||
and references may be valid even when their live definitions no longer exist.
|
||||
|
||||
Select an explicit advanced typed history editor over the existing OpenAPI
|
||||
TraceBaseline/FacetReview descriptors. They provide the finite shape catalog
|
||||
already required for REST parity; final authority remains core typed decoding,
|
||||
historical admission and one EditTrace through Document. Preserve invalid raw
|
||||
drafts, exact historical UUID text, optional/Observed alternatives and explicit
|
||||
constructor selection. Current observations may be copied only by a deliberate
|
||||
action. Cache the descriptor and bound recursive rendering; unsupported shapes
|
||||
must fail visibly instead of generating an invalid default.
|
||||
|
||||
Reject a separate handwritten GUI metamodel or a new duplicate core type-shape
|
||||
catalog, and reject raw JSON as the sole graphical editing surface. Revisit if
|
||||
descriptor/core consistency or native usability cannot be verified. This choice
|
||||
is under implementation; it does not yet close the parity finding in
|
||||
`release-end-state-audit-20260910.md`.
|
||||
|
||||
## Basic canvas authoring — 2026-09-07
|
||||
|
||||
The user rejected the large horizontal creation row. Actual installed Capella 7.1
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
# First release execution plan
|
||||
|
||||
## Active execution — 2026-09-10
|
||||
|
||||
1. Restore a reproducible local toolchain; repair the interrupted compact palette;
|
||||
verify focused history/draft/placement regressions and the actual native workflow.
|
||||
2. Independently reassess current behavior/scenario ergonomics and OA/SA semantic,
|
||||
source/API and documentation completeness. Resolve verified high-impact gaps
|
||||
with bounded changes and cross-representation acceptance criteria.
|
||||
3. Complete realistic graphical OA/SA teaching, transition, behavior, recovery and
|
||||
hosted workflows. Exercise minimum and wide layouts, rejected edits and history.
|
||||
4. Run current Windows/Linux automated and native acceptance, fresh independent
|
||||
end-state reviews, optimized builds and package verification. Publish accurate
|
||||
build/run evidence and handoff only when the original release criteria hold.
|
||||
|
||||
This ordering supersedes historical stopped-session next actions below. Preserve
|
||||
their scoped evidence; no test or runtime result transfers to a new binary without
|
||||
verification. Root holds the serialized build/native lease; Gitea #1 tracks workers.
|
||||
|
||||
**Latest refinement:** consult actual installed Capella 7.1, replace the user-rejected horizontal creation/connection rows with a compact contextual palette and inline authoring, independently critique interaction risks, then build and verify the real workflow at wide/minimum sizes. [Benchmark and concrete acceptance](release-capella-basic-editing-20260907.md). Do not ship the rejected toolbar as the usability correction.
|
||||
|
||||
**Current priority (7 September):** the user's usability correction takes precedence over the advanced feature sequence below. Establish contextual named diagram creation, existing-element placement, basic editing and clear layer transitions, then verify them natively. Desktop checks were explicitly reauthorized. See [priority reset and acceptance](release-basic-editing-reset-20260907.md).
|
||||
|
||||
@@ -1,5 +1,98 @@
|
||||
# Progress Ledger — first release
|
||||
|
||||
## Active continuation — 2026-09-10
|
||||
|
||||
The renewed user request is preserved in `PROJECT_GOAL.md`. Root is integrating
|
||||
the first release, prioritizing direct diagram authoring and behavior/scenario
|
||||
usability. The checkout began clean at `1797516` on `codex/release1-continuation`.
|
||||
Gitea issue #1 and Wiki Home were read; ownership and progress are coordinated there.
|
||||
|
||||
- Root owns integration, serialized builds, native Computer Use and these ledgers.
|
||||
`palette_repair` owns architecture authoring and `diagram.rs`; `behavior_audit`
|
||||
and `release_audit` independently inspect ergonomics and release coverage.
|
||||
- The recorded palette compilation errors have local corrections pending a fresh
|
||||
build. Earlier 584-test evidence does not cover those corrections.
|
||||
- This checkout began without the historical `.local` toolchain/cache/packages.
|
||||
Project-local Rust/Cargo 1.98.1 GNU/LLVM and verified LLVM-MinGW 20260908
|
||||
(clang 23.1.1) are installed. Locked dependencies are being fetched.
|
||||
Host: four logical processors, 8 GiB RAM; builds remain serialized.
|
||||
- Computer Use initialized successfully. The renewed request authorizes native
|
||||
evaluation; old session Escape holds and process IDs are historical. No Arcadia
|
||||
or Capella window is currently running.
|
||||
- WSL lists no installed distribution. Linux runtime/build acceptance remains an
|
||||
open gate; no historical denied guest workaround is being retried.
|
||||
|
||||
**Current branch is `codex/behavior-authoring-20260910`.** A distinct Gitea user
|
||||
`Codex-9e8r6t` is running another root/team on a separate environment with the old
|
||||
build cache. Our identity is **Codex-956atf**. Gitea #25 and #38 acknowledge the
|
||||
scope split: their palette/platform/package and subsequent scenario layout work;
|
||||
our process/scenario adapters and typed-history work. We will publish a clearly
|
||||
untested coherent snapshot on this distinct branch for their warm-cache checks.
|
||||
No shared branch-tip push or merge has occurred here.
|
||||
Their reports/results must not be claimed as local validation or overwritten.
|
||||
|
||||
Implemented locally, awaiting compilation:
|
||||
|
||||
- Palette compile repair, retained inline name after rejected stale Submit,
|
||||
nonfinite click rejection and rename/allocation/history regressions.
|
||||
- One interactive process renderer with presentation-only inferred ordered
|
||||
paths; first editing batch preserves adjacency atomically. Independent source
|
||||
review found no confirmed defect. Root added 3+ step/before-first insertion
|
||||
and incompatible imported conversion tests after the reviewer hit model capacity.
|
||||
- Scenario Action/State/Reuse/empty Fragment tools capture clicked operand/slot
|
||||
and optional lifeline. Pointer-to-App, recovery, stale-event and history tests
|
||||
are written. A bounded exact caller/use return path and scoped mounted-scroll
|
||||
restoration are implemented with regressions, also untested.
|
||||
- Advanced typed stored-history editor uses existing OpenAPI type descriptors;
|
||||
twelve focused tests cover typed selectors, historical IDs, invalid raw input,
|
||||
recovery and source/history. Core validation remains authoritative.
|
||||
- Normative catalogs now include ExchangeCategory, ContextualLink and both
|
||||
Exchange trace pairs. Manual trace help and scope source version corrected;
|
||||
strict Docutils 0.22.2 HTML regeneration passed.
|
||||
|
||||
Reference evidence: actual Capella 7.1 scenario/lifeline/LOOP creation and saved
|
||||
XML are recorded in `release-capella-scenario-benchmark-20260910.md`; the isolated
|
||||
Capella process closed normally and is absent. This is not Studio acceptance.
|
||||
|
||||
Toolchain bootstrap exec session **20812** completed successfully: verified
|
||||
LLVM-MinGW 20260908 ZIP SHA256
|
||||
`1bcf74d06b724aeecaa6412ca85f5b26fb1da770e7cdcefa9263c9c5c3ad34b6`, extracted
|
||||
successfully; stable **1.98.1** GNU/LLVM, rustfmt and Clippy installed.
|
||||
`.local/toolchain/env.ps1` configures task-local Cargo/Rustup/PATH and one build
|
||||
job. Locked dependency fetch is local exec session **13108**. No build/test pass
|
||||
is established; the target cache is still cold.
|
||||
|
||||
The coherent candidate `01bb52a` passed `cargo fmt --all` and `git diff --check`.
|
||||
Direct push failed because this fresh host has no Git credentials. Authenticated
|
||||
Gitea MCP transferred its exact verified Git bundle through
|
||||
`codex/behavior-transfer-20260910` (`harness/transfers/behavior-01bb52a.bundle.b64`),
|
||||
prerequisite `1797516`, decoded SHA256
|
||||
`85A38B00207036DCB8DB24ED4A7E80B2BE670275EEB7B00413D36153322ADA5D`.
|
||||
This **UNTESTED integration snapshot** lets the other host use its warm cache;
|
||||
transfer is not acceptance. Their broader architecture
|
||||
palette/Escape repair should be reconciled with our overlapping compile/stale
|
||||
repairs while retaining the additional invariant tests. Their measured minimum
|
||||
scenario canvas is too small; they own the subsequent layout implementation.
|
||||
Their isolated palette commit is `7d1df4f`, fetched locally for reconciliation.
|
||||
|
||||
Independent F1 review found a real optional-value cache collision in `01bb52a`.
|
||||
The follow-up distinguishes null/scalar/list/tagged constructor identities and
|
||||
adds actual inner selector-event tests. Independent source recheck found no
|
||||
remaining collision; execution is still pending. The original full-platform
|
||||
fetch was stopped after network timeouts; current Windows desktop test build
|
||||
is exec session **51036**, compiling dependencies. Root owns this Cargo lease.
|
||||
|
||||
Next: focused `architecture_authoring_tests`,
|
||||
`behavior::topology`, `direct_clicked`, caller-navigation and `history_fields::tests`,
|
||||
then full relevant gates and a fresh native binary. Root owns all Cargo on this
|
||||
host. Worker output or tests existing in source are not passes.
|
||||
|
||||
Gitea Actions run16 is queued for both ubuntu-latest and windows-latest with no
|
||||
started steps, as are older listed runs. No usable current Linux runner result
|
||||
is established and no duplicate dispatch was made. Progress is concrete;
|
||||
release acceptance remains incomplete. Older checkpoint paragraphs below retain
|
||||
their original revision scope.
|
||||
|
||||
**Checkpoint publication — 2026-09-10:** the user requested committing all current changes and pushing `codex/release1-continuation` to both `origin` and `gitea`. This is a work-in-progress snapshot. The interrupted palette-focused compilation failed with E0659 (ambiguous `column!` in `app/architecture_authoring/palette.rs:168`) and E0507 (moving the view out of an Arc in `diagram.rs:624`); an unnecessary-pattern-parentheses warning also remains in `architecture_authoring.rs:346`. The prior 584-test pass does not cover this newer palette revision. No Cargo/rustc process or local subagent was active at the publication preflight. Fix these errors and complete automated/native acceptance before packaging the redesign. Gitea coordination starts at issue #1 and the project Wiki; older evidence below retains its original scope.
|
||||
|
||||
**Latest priority checkpoint — Capella consultation and redesign:** the user rejected the large creation buttons and explicitly requested consulting the installed Capella. Native control resumed for that consultation in a disposable Capella 7.1 workspace. Actual palette, F2 rename and activity allocation were exercised; saved XML confirms allocation distinct from ownership. The horizontal creation/connection rows are being replaced with a compact expandable palette and inline naming. This supersedes the Escape hold and toolbar-fix proposal below. New implementation, native acceptance and a fresh user build remain pending. [Observed benchmark, alternatives and acceptance criteria](release-capella-basic-editing-20260907.md). The first-release goal remains active.
|
||||
|
||||
@@ -1,5 +1,33 @@
|
||||
# Task Ledger — first release
|
||||
|
||||
## Current knowledge — 2026-09-10
|
||||
|
||||
Given: the renewed objective in `PROJECT_GOAL.md` requires a feature-complete,
|
||||
usable OA/SA first release and prioritizes direct behavior/scenario authoring.
|
||||
Graphical/source/REST parity, explicit reST semantics, traceability, offline
|
||||
guidance, Windows/Linux acceptance and independent review remain required.
|
||||
Older scope deferrals must be checked against this request.
|
||||
|
||||
Verified: the checkout began clean at `1797516`; Gitea contains one active
|
||||
continuation issue and no open PR. Current code includes direct behavior editors,
|
||||
so old blanket descriptions of static diagrams need workflow-level reassessment.
|
||||
The machine began without the ignored historical toolchain/cache/distributions.
|
||||
Local Rust/Cargo 1.98.1 and verified LLVM-MinGW are now installed; dependency
|
||||
fetch is pending and no current binary has been built. Native Computer Use is
|
||||
available. Isolated Capella activity/lifeline/LOOP creation was saved and checked;
|
||||
no Arcadia window is running. Candidate B1/B2/B3 and F1 edits are source-complete,
|
||||
formatted and whitespace-clean, but have no fresh compile/test/runtime pass.
|
||||
|
||||
Assumption: the selected compact architecture palette remains a promising design,
|
||||
pending real authoring evidence. Decision: repair and evaluate it before further
|
||||
redesign. Parallel independent audits identify current behavior and semantic gaps
|
||||
instead of reimplementing integrated work.
|
||||
|
||||
Open questions: which remaining menu-only behavior operations prevent a coherent
|
||||
direct workflow; which first-release semantic/parity gaps remain; and how to
|
||||
complete current Linux runtime acceptance. Root owns integration and native
|
||||
control; worker ownership is disjoint. See `STATE.md` and Gitea #1.
|
||||
|
||||
**Superseding user direction:** large horizontal creation buttons were rejected as intrusive and unprofessional. The user explicitly authorized consulting the installed Capella; that consultation has resumed and established actual compact palette, inline rename and explicit activity-allocation behavior. See [Capella 7.1 evidence and selected redesign](release-capella-basic-editing-20260907.md). The current candidate replaces creation/connection rows with one compact expandable palette and an in-place name editor. Native acceptance of the redesigned executable is pending; compactness is not assumed to prove usability. The earlier Escape hold and proposed toolbar-only correction below are superseded.
|
||||
|
||||
**Latest evidence:** basic editing full desktop584/584, bins, formatting and strictClippy passed. Native named Entity creation works at100%, but changing pending-toolbar height moves the screen placement22px. User Escape stopped the attempted Undo; native checks are paused again. Code-only toolbar correction proceeds. [Current findings](release-basic-editing-reset-20260907.md). The previous goal turn made concrete progress through implementation, integration gates and a native-discovered defect; this is not a no-progress blocker turn.
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
# Behavior authoring audit — 2026-09-10
|
||||
|
||||
Author: Codex-956atf `/root/behavior_audit`. Initial bounded read-only source and
|
||||
domain review, followed by root's separately assigned B2 implementation below.
|
||||
The audit locations/findings describe the pre-edit snapshot. No Cargo,
|
||||
application input, or new runtime acceptance was performed. Read repository
|
||||
AGENTS.md, Gitea issue #1 and
|
||||
its comments, Wiki Home, and open PR inventory (none). Coordination is in issue
|
||||
#1. Source was inspected in the shared continuation checkout while other workers
|
||||
handled architecture authoring/builds; HEAD at the final inspection was
|
||||
`1797516f460ce27f7bc92e50ea2478ec51dd49b9`.
|
||||
|
||||
The current implementation is substantially beyond static behavior diagrams.
|
||||
The remaining high-value work is making direct authoring consistent across the
|
||||
first process contribution and the non-message scenario tools. Historical
|
||||
absence claims in the early semantics audit are superseded by source7 code and
|
||||
its scoped evidence.
|
||||
|
||||
## Ordered findings
|
||||
|
||||
### B1 — A first exchange switches a new process into a less editable surface
|
||||
|
||||
**High workflow impact; source-confirmed, fresh native reproduction required.**
|
||||
|
||||
An empty Process/FunctionalChain opens the editable topology Graph. Draw its
|
||||
first Exchange contribution and it becomes a nonempty exchange-only ordered
|
||||
path. `uses_explicit_topology` now returns false, so the view changes to a
|
||||
sequence Scene without topology interaction. Tools for drawing contributions,
|
||||
connecting, reconnecting, zooming and reordering disappear. A one-contribution
|
||||
path has no link to convert, so the user must use Append exchange or separate
|
||||
Add work/control/reuse form buttons to return to the graph. Existing multi-step
|
||||
ordered paths likewise require the conversion/menu route.
|
||||
|
||||
Evidence: `behavior.rs:753` defines the inferred legacy/explicit distinction;
|
||||
`behavior/topology/view.rs:89` selects the explicit surface, `:380` begins the
|
||||
legacy branch, and `:421` builds `Scene::sequence` without an interaction
|
||||
context. `behavior.rs:887` initializes `interaction: None`.
|
||||
`app/process_canvas.rs:316` already tests first/second exchange creation and
|
||||
exact history, but that test does not assert a continuous editable surface.
|
||||
|
||||
**Reproduce:** Create an empty OA process (repeat in SA), choose Exchange and
|
||||
click the canvas, select an existing exchange and Create. Try to draw a Work
|
||||
contribution or connect from the newly created contribution without entering a
|
||||
different workflow. Repeat with an old ordered exchange-only process.
|
||||
|
||||
**Bounded next fix:** retain one interactive process surface for empty, single
|
||||
and legacy ordered paths. Preserve the legacy implied order; when a pointer
|
||||
mutation needs explicit links, include the existing `preserved_order` proposal
|
||||
in that same atomic command. Do not invent a self-link merely to force the view
|
||||
predicate, and do not migrate a document merely by opening it.
|
||||
|
||||
**Acceptance:** first and subsequent contributions remain directly editable;
|
||||
opening/selecting remains read-only; OA and SA preserve all old exchange IDs
|
||||
and implied order; insertion/connection uses one exact source/model Undo step;
|
||||
redo and save/reopen retain the result. Verify the actual viewport and visible
|
||||
tools at 1100×700 and a normal desktop size.
|
||||
|
||||
### B2 — Most scenario objects still require detached append forms
|
||||
|
||||
**High workflow impact; source-confirmed, fresh native evaluation required.**
|
||||
|
||||
Scenario canvas tools are only Select and Message. Lifeline, reuse, fragment,
|
||||
action and state-observation buttons directly start a form. Every such Start
|
||||
sets the parent to the main scenario and the insertion position to append,
|
||||
even when an operand is selected. Consequently adding an action at a visible
|
||||
operand position or inserting a reference/fragment between existing messages
|
||||
requires manually choosing an operand, creating at its end, then reordering.
|
||||
Fragments cannot be drawn around a visible sequence range. The semantics and
|
||||
generic property editing exist; the missing part is contextual canvas authoring.
|
||||
|
||||
Evidence: `behavior/scenario.rs:69` (`CanvasTool`), `:327` (Start initializer),
|
||||
`:1436` (form-launch controls), `:1565` (manual destination), `:1777` (fragment
|
||||
form). `behavior.rs:1564` handles only message drawing and selected-occurrence
|
||||
reordering. Reordering deliberately refuses operand changes at
|
||||
`behavior/scenario.rs:636`; this invariant should remain explicit.
|
||||
|
||||
The scenario shell also retains three permanent control rows, an outer page
|
||||
scroll and a separately scrolled canvas; pending forms add another scroller.
|
||||
The canvas is capped at 340 px normally and 190 px while pending (`:1535`),
|
||||
unlike the newer process remaining-height layout. This is a source-established
|
||||
space allocation; clipping or interaction failure on the current binary is
|
||||
not claimed without native observation.
|
||||
|
||||
**Reproduce:** In the teaching scenario select an ALT operand and a middle
|
||||
message. Add Action or Reuse. The form says Main scenario sequence / append,
|
||||
rather than retaining the selected operand and visible insertion point. Try
|
||||
the same task by selecting a tool and clicking the intended canvas position.
|
||||
|
||||
**Bounded next fix:** extend the existing scenario gesture/Slot adapter for
|
||||
Action, State, Reuse and Fragment; capture the exact owner, operand, before-ID
|
||||
and optional lifeline before opening its compact contextual editor. Reuse the
|
||||
existing core commands, validation, draft recovery and atomic history. Give
|
||||
scenario controls the compact, stable remaining-height shell used by processes.
|
||||
Range wrapping and cross-operand movement need a distinct explicit transaction;
|
||||
they should not be silently smuggled into ordinary reorder.
|
||||
|
||||
**Acceptance:** create each supported non-message occurrence at the clicked
|
||||
root/operand slot; select its semantic identity afterwards; Escape, scroll,
|
||||
resize, document change and rejected Apply cannot commit a stale slot; invalid
|
||||
draft text survives repair/recovery; Undo/Redo restore complete source bundles.
|
||||
Native users must complete an ALT + action + reuse task at 1100×700 without
|
||||
losing the target region or searching for offscreen submission controls.
|
||||
|
||||
### B3 — Opening a reused scenario does not retain an explicit caller return
|
||||
|
||||
**Medium workflow impact; source-confirmed.**
|
||||
|
||||
Scenario references have real identity and bindings, and an Open action works.
|
||||
However it is placed in the full sequence listing below the canvas and sends
|
||||
only `Message::Select(target)`. Selection replaces the active scenario. There
|
||||
is no scenario caller stack/Back action to restore the particular use and its
|
||||
viewport, unlike process reference navigation. The Connections inspector can
|
||||
find users manually; that does not preserve which of several callers was open.
|
||||
|
||||
Evidence: `behavior/scenario.rs:1841` / `:1893` (listing/Open),
|
||||
`app.rs:961` / `:982` (selection changes active behavior),
|
||||
`behavior/scenario.rs:155` (State has no return context). Compare existing
|
||||
`behavior/topology/direct.rs:191` (remember/back) and
|
||||
`app/process_canvas.rs:161` / `:183` (guarded Open/Back).
|
||||
|
||||
**Reproduce:** Open caller A containing two uses of the same scenario, select
|
||||
its second use in the canvas, open the definition through the listing, then
|
||||
return to that exact use and viewport. The user must reconstruct the return
|
||||
through owner selection/Connections.
|
||||
|
||||
**Bounded next fix:** selected-reference Open and Back to caller controls with
|
||||
document/owner/use identity and viewport context, using the same draft guards
|
||||
as process navigation. This is transient navigation state, not model history.
|
||||
|
||||
**Acceptance:** A→B→C and two uses of B return in order to the exact initiating
|
||||
use; renamed/deleted callers are handled safely; pending invalid edits block
|
||||
navigation without loss; no model/source/Undo entry is added by navigation.
|
||||
|
||||
### B4 — Create/Delete message semantics are not reflected in lifeline geometry
|
||||
|
||||
**Medium visual-semantic impact; source-confirmed.**
|
||||
|
||||
The core validates creation/destruction ordering, and message names distinguish
|
||||
Create and Delete. The renderer nevertheless draws every lifeline from its
|
||||
top header to the bottom of the whole scenario; lifecycle kinds only affect
|
||||
the message label. There is no birth anchor or destruction mark, so a reader
|
||||
sees the same apparent existence interval for an ordinary participant and one
|
||||
that is explicitly created and destroyed during the interaction.
|
||||
|
||||
Evidence: `behavior.rs:1016` always builds the full-height lifeline;
|
||||
`:1165` selects only a textual message-kind prefix. Core lifecycle coverage is
|
||||
in `oa-core/tests/release_scenarios.rs:459` and its loop/branch variants. This
|
||||
finding is about visualization, not missing core lifecycle validation.
|
||||
|
||||
**Reproduce:** Use a valid root sequence with Create(B), a call/reply involving
|
||||
B, and Delete(B). Inspect B before Create and after Delete; its lifeline still
|
||||
runs through both regions.
|
||||
|
||||
**Bounded next fix:** derive birth/death anchors and a destruction glyph from
|
||||
the existing occurrence identities. Handle branch-local lifetime explicitly;
|
||||
an event in one ALT operand must not falsely terminate all other alternatives.
|
||||
No persistent execution engine is needed.
|
||||
|
||||
**Acceptance:** root-sequence Create/Delete, ordinary lifelong participants,
|
||||
ALT-specific events and LOOP lifecycle warnings are distinguishable; hit tests
|
||||
still return the actual occurrence; direct insertion continues to reject invalid
|
||||
before-birth/after-death uses through the shared core boundary.
|
||||
|
||||
## Semantics and history findings that must not be reported as missing
|
||||
|
||||
- Scenario occurrences and referenced engineering definitions are distinct;
|
||||
repeated exchange uses have distinct names/IDs. Direct Call+Reply creates
|
||||
correlated occurrences in one transaction. Execution bars are derived from
|
||||
correlation and operand boundaries (`behavior.rs:1304`), not persisted proof
|
||||
of simulated execution. `scenario.rs:1026` and direct tests cover the adapter;
|
||||
the earlier scoped native results are in `release-scenario-editing.md`.
|
||||
- Source7 already provides caller-qualified process addresses, distinct repeated
|
||||
reference uses, precedence versus exchange/common-work incidence, and explicit
|
||||
interface-operation allocation identity. See `oa-core/src/model.rs:534`,
|
||||
`behavior/topology/contextual.rs`, `scenario_operation_tests.rs`, and
|
||||
`release-source7-native-acceptance-20260907.md`. The original S1/S2 absence
|
||||
findings are stale. Remaining purpose/native cases are acceptance work.
|
||||
- Shallow/deep history vertices, scope validation, creation tools and documented
|
||||
semantics already exist (`oa-core/src/state_validation.rs:187`,
|
||||
`oa-core/tests/release_states.rs:466`, `behavior/state_view.rs:111`,
|
||||
`docs/language.rst:860`). Shallow history restores the immediate substate;
|
||||
deep history restores nested configuration. Studio explicitly uses history
|
||||
per Region and does not persist running execution state. The primary Capella
|
||||
metamodel describes the corresponding containing-state behavior; this is a
|
||||
documented profile distinction, not grounds to require a simulator.
|
||||
[CapellaCommon schema](https://raw.githubusercontent.com/eclipse-capella/capella/master/m2/plugins/org.polarsys.capella.core.data.def/model/CapellaCommon.ecore).
|
||||
- Capella has distinct start/finish duration constraints and execution objects.
|
||||
Studio's local timer is not an equivalent named duration relation. It does
|
||||
have maintained generic constraints over semantic IDs. This audit did not
|
||||
falsify that alternative for a required user workflow, so it does not reopen
|
||||
a metamodel expansion already assessed in `release-scope-reconciliation-20260906.md`.
|
||||
[Capella Interaction schema](https://raw.githubusercontent.com/eclipse-capella/capella/master/m2/plugins/org.polarsys.capella.core.data.def/model/Interaction.ecore).
|
||||
|
||||
## Comparison and next action
|
||||
|
||||
The existing native Capella benchmark actually exercised palette→canvas chain
|
||||
reference/control/sequence-link creation, a scenario reference region and Open,
|
||||
and call/return insertion reusing an existing exchange. It separates observed
|
||||
controls from executed actions (`release-capella-workflow-benchmark.md:51–69`).
|
||||
These support B1–B3 as engineering-workflow comparisons rather than a demand to
|
||||
copy the Eclipse UI. Capella's official overview independently distinguishes
|
||||
functional, exchange and interface scenario perspectives, all now represented
|
||||
by Studio's typed scenario viewpoints.
|
||||
[Official feature overview](https://mbse-capella.org/features.html).
|
||||
|
||||
Prioritize B1 and the bounded slot-based part of B2; native-check those exact
|
||||
first-use tasks after root's current compilation/runtime gates. B3 is a small
|
||||
navigation follow-up. B4 should receive branch-aware visual acceptance. None
|
||||
of this report certifies current Windows/Linux behavior or transfers historical
|
||||
test/native evidence to a changed executable.
|
||||
|
||||
## Authorized B2 implementation follow-up
|
||||
|
||||
Root subsequently assigned `behavior.rs`, `behavior/scenario.rs` and only the
|
||||
ScenarioCanvas event arm in `app.rs`; Gitea #18 records this ownership under
|
||||
Codex-956atf. The separate topology worker owns B1. Current changes:
|
||||
|
||||
- A compact selector replaces the three permanent scenario tool rows. Action,
|
||||
Observe state, Reuse and empty Fragment use the same choose-tool/click-position
|
||||
workflow as messages; lifeline creation remains explicit because persistent
|
||||
horizontal placement is outside the existing schema.
|
||||
- Press captures the exact root/operand/before identity and an optional clicked
|
||||
lifeline for Action/State. Release opens the existing draft; no model mutation
|
||||
occurs until Create. The selected position remains marked in the pending
|
||||
canvas. Clicking away from a lifeline leaves it unresolved for explicit choice.
|
||||
- Existing context, scroll/bounds, Escape and pending-edit guards apply to this
|
||||
new press variant. The draft adapter validates slot and lifeline ownership
|
||||
before starting. Existing command admission handles the complete semantics;
|
||||
no separate model, schema, fragment wrapping or cross-operand reorder was added.
|
||||
- Added `direct_clicked` tests: real Scene callbacks through App for all four
|
||||
tools, root and nested slots, on/off-lifeline clicks, exact full source/model
|
||||
Undo/Redo; invalid/cross-scenario capture, all context dimensions, scroll
|
||||
invalidation, incomplete raw-draft recovery/repair; all four tools against
|
||||
nine pointer cancellation cases. These tests are written, not yet run.
|
||||
- Applied root's coordinated process-header predicate change so every
|
||||
Process/FunctionalChain uses the compact header with the topology worker's
|
||||
unified renderer. The semantic legacy/explicit predicate is unchanged.
|
||||
|
||||
Whitespace verification passed for this worker's source changes. Another worker
|
||||
read the clicked-slot adapter and reported no confirmed blocker, while requesting
|
||||
the now-added off-lifeline/cancellation cases. Formatting/build/tests await root's
|
||||
toolchain and serialized Cargo lease. No native pass is claimed: test actual
|
||||
root/operand selection, current tool visibility, pending marker/viewport and
|
||||
Create/Cancel reachability at 1100×700 and normal size, including crash recovery,
|
||||
save/reopen and exact Undo/Redo. B2's larger remaining-height shell, fragment
|
||||
range wrapping and B4 lifecycle rendering are not part of this bounded
|
||||
implementation. B3 followed as the separately assigned slice below.
|
||||
|
||||
## B3 implementation follow-up — candidate ready, execution pending
|
||||
|
||||
The selected ScenarioUse exposes **Open definition**; the target scenario exposes
|
||||
**Back to caller**. Detailed sequence-list Open uses the same guarded adapter.
|
||||
Navigation retains document, caller scenario, exact use occurrence, target,
|
||||
selected tool and independent canvas/page offsets in at most 32 transient frames.
|
||||
Nested A → B → C and distinct uses of B return to their initiating occurrence.
|
||||
Renaming retains identity; deleted or repointed uses are rejected on return.
|
||||
Property, source and behavior drafts block both directions without applying them.
|
||||
|
||||
The existing scroll observer restores only its matching document/scenario/token,
|
||||
directly in the mounted root Scrollable. It observes natural viewport clamping,
|
||||
consumes a request once and sends a correlated receipt to the App. It does not
|
||||
use a delayed global scroll task or traverse into nested scrollables. This adds
|
||||
one passive Restored variant beside the existing Scrolled early App handler; all
|
||||
other App changes remain in ScenarioCanvas. Caller navigation is neither model
|
||||
content nor recovery data and creates no model/source/history command.
|
||||
|
||||
Added regressions cover nested and distinct-use App navigation, exact unchanged
|
||||
model/source/revision/history, invalid pending drafts, four stale context axes,
|
||||
non-use targets, renamed/deleted/repointed caller references, and real mounted
|
||||
Scrollable → App flow for both scroll areas including clamping, subsequent wheel
|
||||
input, repeated receipts and changed page. Foreign document/owner and process
|
||||
views reject restoration. These tests have not run yet; root will format and
|
||||
compile the coherent candidate before acceptance.
|
||||
|
||||
Native acceptance remains: select a root or operand reference, Open definition,
|
||||
open a second reference there, return twice and inspect exact selected occurrence,
|
||||
tool and both scroll positions. Repeat from the detailed list with two uses of
|
||||
one target, a resized window and an invalid draft. Verify no dirty/history change
|
||||
from navigation. The separate remaining-height layout slice must retain these
|
||||
events and observer restoration when moving the scenario workspace.
|
||||
@@ -0,0 +1,44 @@
|
||||
# Native Capella scenario benchmark — 2026-09-10
|
||||
|
||||
Codex-956atf /root used the installed Capella 7.1 executable through Computer
|
||||
Use in an isolated `.local/capella-reference-20260910` workspace. The generated
|
||||
project is `Behavior workflow benchmark`; no existing user model was opened.
|
||||
This evaluates reference-product behavior, not the current Studio binary.
|
||||
|
||||
At the observed 1010×760 window size:
|
||||
|
||||
- Operational Analysis → Define Operational Activities and describe Interactions
|
||||
exposes creation of activity breakdown, interaction and activity-scenario views.
|
||||
The scenario workflow created `[OAS] Response workflow` and its capability.
|
||||
- The right scenario palette exposes Activity, Activities, Interaction,
|
||||
Interaction with return branch, Reference, LOOP and Operand. Activity is a
|
||||
direct point-placement tool: clicking the canvas created a represented activity
|
||||
and a lifeline. The lifeline is a separate InstanceRole, not the work definition.
|
||||
- Selecting LOOP and dragging a region across the lifeline created a visible
|
||||
combined fragment. Its tooltip explains the repeated scenario part and covered
|
||||
lifelines. The actual drag created one operand and explicit start/end records.
|
||||
- Selecting the new object expanded formatting/context controls and moved the
|
||||
canvas vertically. Preserve direct interaction and coverage meaning in Studio
|
||||
without copying this unstable canvas origin or the clipped palette labels.
|
||||
|
||||
Ctrl+S saved the model, and the saved XML independently confirms:
|
||||
|
||||
| Record | Identity / relationship |
|
||||
|---|---|
|
||||
| Activity | `04b58488-e33b-4693-ae8d-6f830cbc0cf3` |
|
||||
| Scenario | `49f702e0-1e0d-45c2-a0e8-e40545ce5b6c`, kind INTERACTION |
|
||||
| InstanceRole | `29115069-194d-4c1a-ac2c-ee86486a04fb`, represents the Activity |
|
||||
| LOOP | `1b4a102e-4c20-4e92-bd4f-71e6b677d2fa` |
|
||||
| Operand | `ac05a0af-60d1-48f2-9548-17214d3ebdc1`, covers the InstanceRole |
|
||||
| Fragment interval | start `8d988d2b-c191-46b2-8c92-1ff1e62251cb`, end `a8c26302-a5bb-49c0-8e7c-d2fe8026fe1a` |
|
||||
|
||||
These observations support scenario tool/point authoring and explicit occurrence
|
||||
versus definition identity. They do not demonstrate wrapping existing messages,
|
||||
cross-operand movement, lifecycle drawing or successful scenario reuse; those
|
||||
operations were not exercised. Studio's current clicked-slot improvement creates
|
||||
an empty typed fragment using its existing occurrence/branch model. Persisted
|
||||
horizontal lifeline arrangement and drag-range wrapping need separate design and
|
||||
acceptance if adopted; this benchmark does not silently introduce either.
|
||||
|
||||
After saving, Alt+F4 requested a clean Capella close. Root checks process absence
|
||||
before reclaiming memory for compilation; no forced termination was used.
|
||||
@@ -0,0 +1,90 @@
|
||||
# Independent release end-state audit — 2026-09-10
|
||||
|
||||
Reviewer: **Codex /root/release_audit**. Base HEAD `1797516f460ce27f7bc92e50ea2478ec51dd49b9`; concurrent palette/integration work is excluded from acceptance. Read PROJECT_GOAL.md, the supplied request, AGENTS.md, Gitea issue #1/comments, Wiki Home and the empty open-PR inventory before auditing. Coordination is recorded in issue #1.
|
||||
|
||||
**Verdict: first-release completion is not supported yet.** Current source supplies substantial OA/SA semantics and shared source/GUI/REST infrastructure. The earlier missing communication means, selective views, coverage diagnostics, contextual assembly and operation identity are implemented; historical absence findings must not be repeated as current defects. A concrete trace-inspector defect remains below, and current native/platform/package acceptance is still incomplete.
|
||||
|
||||
This was read-only implementation/specification inspection. No Cargo, native automation, deployment or runtime test ran. Test names below identify evaluators that exist, not fresh passing results. Root owns current builds/runtime and the separate behavior audit owns process/scenario ergonomics.
|
||||
|
||||
## Requirements matrix
|
||||
|
||||
| User outcome | Current source evidence | Acceptance assessment |
|
||||
|---|---|---|
|
||||
| Rust/Iced desktop on Windows and Linux | Three Rust crates; desktop Cargo.toml selects Iced 0.14, WGPU, X11 and Wayland. README gives native build paths. | Implemented architecture; current Windows palette rebuild/native acceptance and current Linux runtime remain gates. An ELF cross-build is not a Linux GUI trial. |
|
||||
| Useful OA and SA engineering semantics | model.rs has operational roles/activities/missions/processes, system boundary/functions/chains, typed data/interfaces/ports, states, scenarios, constraints and 25 relation kinds. validation.rs, ports.rs and data.rs enforce common rules. | No new fundamental absence established outside the separate behavior audit. Do not infer full Capella conformance. LA/PA and execution remain outside the user's current layer scope. |
|
||||
| Equivalent persistent graphical/text models | Project, validated Command boundary, source_project updater and desktop Document couple accepted source to the same model. forms.rs exposes typed records; dedicated workflows handle richer constructs. | Strong implementation coverage; **F1 exposes a broken typed-history inspector path**. `every_demo_record_has_a_lossless_graphical_draft` checks existing demo values, not every exposed constructor/add operation. |
|
||||
| Strict, unambiguous reST contract | docs/language.rst defines grammar, identity, invariants, lifecycle, migration and graphical obligations; docs/api.rst specifies REST. | Substantial contract; **F2 catalog inconsistencies** need correction. Source/header and package documentation should agree at final freeze. |
|
||||
| Human/AI/Git-friendly source with highlighting and lint | Modular includes, nested declarations, stable aliases/UUIDs, source-preserving updates and exact source history; highlight.rs adapts core tokenizer; source_workspace/source_navigation retain and locate drafts. | Implemented, with source7 source/lifecycle tests. No new text-loss counterexample found in this bounded audit. LSP, completion and semantic Git merge were not explicit requirements. |
|
||||
| Easy bidirectional OA/SA transition | transitions.rs maps dependencies/counterparts in either direction; semantic review supports scalar, constructor, keyed/ordered, attached and artifact decisions. Communication boundary ports and technical-operation omission are explicit. | Core/API paths and tests exist; actual complete engineering transition and revision/history workflows still require their remaining native cases. **F2 help wording misstates Manual-trace behavior.** |
|
||||
| Trace arbitrary development artifacts | ArtifactLink stores owner/relation/URI/revision/fragment; shared identity/reference maintenance, forms and searchable traceability inventory; server artifact/trace queries plus ordinary transactions. | Required reference model is present. No artifact fetching, executable adapter or plugin ABI is necessary to establish this requirement. Native final-tree evidence must include save/reopen and a cross-tool REST edit. |
|
||||
| Deployable service and clear REST for automation | routes.rs exposes project/model/source-bundle, lists/query/validation, commands, transitions and semantic review; store persists coupled representations with ETags. Dockerfile and systemd recipe exist. | Source7 HTTP test explicitly exercises instance/operation edits, parity and restart. Current Linux server suite and exact release build/deployment verification remain outstanding; recipes do not prove deployed execution. |
|
||||
| Intuitive nested diagrams and consistent editing | Distinct semantic ownership, allocation and placement; independent saved views, ports/routes, filters and duplication; current compact palette under repair. | Cannot accept from source. Root's native authoring evidence and separate behavior audit must cover selection, draft/cancel/history and wide/minimum size. |
|
||||
| Built-in concise navigable guide | guide.rs has searchable topics, related links, destination links and worked source7/transition/evidence material; bundled teaching source exists. | Implemented; guide still contains older control wording. Remaining complete teaching workflow and current control-label/native checks must close. |
|
||||
|
||||
## Material findings
|
||||
|
||||
### F1 — P2: the trace-history inspector offers an Add action that cannot create a valid fact
|
||||
|
||||
The generic trace inspector recursively displays `baseline.semantic.facets`. Its array control always offers **+ Add** (`crates/oa-desktop/src/forms.rs:1916–1930`). `Message::FieldArray` forwards directly to `Draft::array` (`app.rs:1720`). `edit_array` has special constructors for bindings, values, routes, waypoints and port anchors, but none for facets: its fallback appends the nil UUID **string** (`forms.rs:622–650`). `SemanticBaseline.facets` instead requires `Vec<FacetReview>` (`crates/oa-core/src/semantic/types.rs:258`), a closed typed enum. `Draft::command` deserializes the trace as-is (`forms.rs:600`). The generated draft therefore cannot become a valid EditTrace.
|
||||
|
||||
Small reproduction for root's serialized test gate: select a trace with `baseline.semantic = Some(SemanticBaseline::default())`, call `Draft::from_project`, then `array("/baseline/semantic/facets", None, 0)` and `command()`. The Add produces a UUID rather than a facet; command must reject. Removing the bad row or reverting recovers the draft, so this is not an established stored-data corruption finding.
|
||||
|
||||
The specialized semantic-review workflow can create reviewed current observations and reset exact history; that does not make this separate advertised Add control valid. Optional semantic history is also omitted when absent, so the ordinary inspector cannot initialize it through that path. The normative obligation explicitly includes typed trace baselines (`docs/language.rst:1959–1963`).
|
||||
|
||||
**Next action:** choose one coherent history-editing surface. Replace the generic invalid constructors with an intentional typed workflow, or route the inspector to the existing review/reset tools while defining which history operations they support. Verify all displayed history controls against absent, empty and nonempty histories, including historical references whose live objects were deleted. Do not claim that a generic recursive form proves full constructor parity.
|
||||
|
||||
### F2 — P2: the normative catalogs and guide give inconsistent transition information
|
||||
|
||||
- `docs/language.rst:1192–1193` lists Exchange traces only as Interaction → FunctionalExchange, although its communication section (`:341`) and `transitions.rs::exchange_counterparts` admit CommunicationMean → BoundaryExchange. The reference table should describe both admitted pairs.
|
||||
- The same document's element catalog (`:94–145`) omits ExchangeCategory, and the relation catalog (`:232–303`) omits ContextualLink. Both have detailed definitions later, so their semantics are implemented/documented rather than absent; the purported catalogs are incomplete entry points for an independent implementation.
|
||||
- `guide.rs:658` says a Manual trace records correspondence “without propagating properties.” Existing-counterpart transition logic finds any endpoint-matching trace and applies reviewed name/description changes without a Manual-kind exemption (`transitions.rs:1055–1153`). Explicit Capability ↔ Mission Manual mappings are deliberately supported (`language.rst:1218`). Clarify that ordinary edits never synchronize automatically, while an explicit supported review can propagate selected properties; do not imply Manual is a no-update policy.
|
||||
- `docs/scope.md:16` still labels the implemented source profile `arcadia 6`; core/schema and the normative contract are version 7. README also retains older review labels and pending claims that newer scoped STATE evidence supersedes. Existing guide Activity/Function steps (`guide.rs:265`) describe the older Create-toolbar workflow while palette work is actively changing that surface.
|
||||
|
||||
**Next action:** synchronize the tables, scope and task instructions against the accepted controls; regenerate the checked-in HTML from corrected reST and rerun the existing strict documentation/OpenAPI checks. Do not change transition semantics merely to match stale wording.
|
||||
|
||||
### F3 — P1 release gate: current platform, deployment and full-workflow evidence is incomplete
|
||||
|
||||
`harness/STATE.md` explicitly records a frozen Linux GUI cross-build with no actual Linux GUI runtime (`:57`), an earlier Linux server HTTP 50/51 result with a subsequently corrected evaluator (`:44`), and pending native purpose/teaching/hosted/compact cases. The Windows development preview (`:46`) predates current palette changes and is not the optimized final release. README correctly calls final suites, optimized/MSRV builds and current packages outstanding (`:156`). Root's new checks may supersede particular rows after this audit.
|
||||
|
||||
This is missing release evidence, not proof that Linux, REST or the implemented workflows are broken. It still prevents meeting the user's explicit build/launch, real-workflow, current-platform and final-review criteria. The Docker builder pins Rust 1.90 while normal rust-toolchain.toml selects stable; final compatibility must be checked against the actual locked tree and intended package, rather than inferred from a different frozen snapshot.
|
||||
|
||||
**Next action:** finish the current Windows authoring slice, then freeze one candidate revision for appropriate complete tests, strict checks, optimized desktop/server builds, current Linux runtime and representative cross-representation/REST restart cases. Assemble packages from that accepted tree and verify examples, reST/HTML/OpenAPI, notices, runtime dependencies, clean launch and checksums. Respect prior runtime/approval boundaries; a stopped guest or denied workaround is not successful acceptance.
|
||||
|
||||
## Highest-value completion sequence
|
||||
|
||||
1. Reproduce and correct F1; retain focused failure/repair evidence. Finish root's current palette compile/native gate and address the separate behavior audit's verified findings.
|
||||
2. Close the already documented complete teaching story: OA responsibility/communication → reviewed SA contribution and interfaces → behavior/degraded state → artifact evidence → source edit → conditional REST edit/reload and stale-write rejection → save/reopen/history. Use exact whole-model and authored-source comparisons, not screen appearance alone.
|
||||
3. Correct F2 and prune misleading current-status wording without deleting historical evidence. Keep the normative contract explicit about intentional asymmetry: semantic ownership versus allocation, OA abstraction versus SA technical ports, and reviewed counterpart differences.
|
||||
4. Complete F3 on a frozen candidate and obtain fresh independent runtime/UX plus final requirements review. No additional broad metamodel subsystem is justified by this audit alone.
|
||||
|
||||
At the end of the initial audit, only this report had been written. The subsequently authorized F1 implementation is recorded below; no fresh runtime/test pass is claimed here.
|
||||
|
||||
## Bounded F1 remediation recommendation
|
||||
|
||||
Root requested a decision on generated review history versus arbitrary facet constructors after receiving this audit.
|
||||
|
||||
**Use the existing semantic-review/history surface, with a distinct typed stored-observation editor.** Removing generic Add and routing to inspection/reset safely removes the immediate trap, but inspection/reset alone does not discharge the current parity contract. Section 5.1 deliberately admits historical observations independently of current object applicability, including references whose live objects are gone; section 8 explicitly requires graphical editing of trace baselines. A workflow that can only acknowledge current live facts cannot express every admitted source-authored historical value. Changing the spec to declare those properties uneditable would narrow the requested equivalence.
|
||||
|
||||
The bounded implementation need not build another reconciliation engine:
|
||||
|
||||
- Choose a closed facet selector and its exact typed row keys, using the existing shared catalog. Never append an arbitrary default FacetReview.
|
||||
- Edit canonical OA and SA Observed branches with their selector-specific types. NotApplicable, Value(none), and absence of the facet remain distinct. Current observations can be presented as a deliberate starting choice, not silently acknowledged on Add.
|
||||
- Keep stored-observation editing separate from Keep/Take/live candidate edits. It changes the trace history only and must say so. Existing inspect/reset remains the ordinary workflow.
|
||||
- Historical UUID input must retain deleted identities and allow syntactically valid exact keys; live-object-only pickers are insufficient. Use the existing historical validator, not live-reference applicability, for historical admission.
|
||||
- Stage a complete typed candidate; validate and apply one EditTrace through the existing Document/source/history boundary. An incomplete selector, key or observation keeps Apply disabled or fails with a located editable draft. Source writeback and Undo/Redo remain shared.
|
||||
|
||||
Acceptance should cover absent history, an empty facet list, one scalar, one optional value, NotApplicable, a keyed attachment/artifact observation with deleted UUIDs, changing an observation's constructor, duplicate/overlapping-invalid history, and exact source/JSON Save/Undo/Redo. Prove the original nil-UUID Add cannot occur. This is a finite history-authoring slice over existing types, not a new metamodel or automatic synchronization feature.
|
||||
|
||||
## F1 implementation candidate — Codex-956atf /root/release_audit
|
||||
|
||||
Root authorized the bounded implementation in `crates/oa-desktop/src/forms.rs`, `forms/history_fields.rs`, and `forms/history_fields/tests.rs`. No core API or app handler was changed. The trace inspector replaces its recursive baseline form with a collapsed **Edit stored history (advanced)** surface. It initializes absent prose/semantic baselines, chooses a facet family and leaf explicitly, edits keyed selectors and both typed observations, and removes/reorders rows. Optional values, NotApplicable, booleans, integers/reals, enum and compound constructors, exact strings and historical UUIDs are separate controls. Current counterpart observations are an explicit opt-in action using `capture_facet`; the editor changes no counterpart.
|
||||
|
||||
**Design decision:** reuse the existing checked-in OpenAPI TraceBaseline/FacetReview descriptor as finite historical field-shape metadata, cached once. This already documents the admitted serialized constructors, so using it here avoids a second manually maintained semantic catalog or new core metadata API. This is not an all-product REST-schema form generator. The supported local refs, oneOf/anyOf, fixed objects, arrays and primitive shapes are bounded by depth 32; list rendering pages 12 rows. Unsupported metadata produces a visible unavailable-field message. The real Rust types and `validate_semantic_baseline` remain authoritative for admission; the descriptor supplies no replacement validator.
|
||||
|
||||
**Deliberate draft behavior:** a newly chosen facet has both observations NotApplicable and cannot be committed until the engineer supplies an admitted observation. New UUID slots are empty, never a made-up nil identity. Historical typed Add replaces the generic UUID-array fallback. Scoped shape/list events preserve unrelated incomplete inputs. Constructor caches follow row movement/removal and recovery; exact-string mode also converts inactive textual buffers transactionally. Replacing or clearing a whole history subtree discards its inactive constructor buffers so later new facts cannot revive old data. Ordinary Apply remains the existing EditTrace/Document/source transaction.
|
||||
|
||||
Ten targeted tests are present. They cover absent/empty/existing history; descriptor-derived complete witnesses for every offered scalar/keyed fact against actual FacetReview deserialization and historical validation; rejected NA/NA and duplicates with unchanged Document snapshots; deleted UUID keys/observations; numeric limits and invalid raw repair; compound values and constructor switching; TypeRef's nested primitive picker; exact Unicode/quoted strings and literal keys; cache relocation/recovery; clear/recreate isolation; source/JSON round-trip and exact one-step Undo/Redo including retained source comments.
|
||||
|
||||
**Verification status:** `git diff --check` passes for the candidate (an unrelated generated HTML line-ending warning is present). No Cargo, rustfmt, native GUI or integration suite was run by this worker; root owns the serialized build/toolchain lease. F1 is therefore a ready source candidate, not an accepted fix yet. Root should run targeted history tests, the relevant desktop suite and strict lint, then exercise expanded history controls at normal/minimum width, typed Add/repair, paging, apply/reopen and Undo in the actual desktop. F2 and shared decision/wiki synthesis remain root-owned. F3 remains open.
|
||||
|
||||
**Independent-review follow-up:** behavior_audit found a real defect in snapshot `01bb52a`: the reused constructor-cache key mapped null, numbers, booleans and arrays to the same empty tag, also colliding with empty UUID input. The nested optional picker consequently restored the old branch immediately instead of switching; earlier tests that injected complete Observed values skipped that event. The bounded follow-up adds history-only typed constructor identities (including distinct enum and tagged-object variants) while retaining other property-editor behavior. Two additional tests derive the visible nested picker choices and dispatch their actual FieldShape events: absent/present integer with invalid raw recovery and exact Value(None); empty UUID/absence with deleted identity; optional list/absence with nested Integer/String constructor retention and recovery. The resulting accepted models still receive exact Document/source/Undo checks. Twelve history tests are now present. Whitespace check passes; compilation, execution and native acceptance remain pending with root.
|
||||
Reference in New Issue
Block a user