Compare commits
77
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd95bd1f23 | ||
|
|
f810fb40eb | ||
|
|
d4d50f061b | ||
|
|
57f07c83c4 | ||
|
|
bcddaba399 | ||
|
|
e79645a0ec | ||
|
|
763e6d8256 | ||
|
|
69c784f204 | ||
|
|
fb9b0c5caa | ||
|
|
6690ed097e | ||
|
|
b86c8dbc40 | ||
|
|
154bcaf04d | ||
|
|
7a02333c4a | ||
|
|
d99e1a369d | ||
|
|
1783658723 | ||
|
|
d5b37f13fd | ||
|
|
b2ef3f5457 | ||
|
|
6c0d9e807c | ||
|
|
f130a365d5 | ||
|
|
1c2798b9b1 | ||
|
|
a2c7817734 | ||
|
|
81ab373f8e | ||
|
|
dc87babf0f | ||
|
|
852a79b566 | ||
|
|
76acfb1108 | ||
|
|
47f8d0f544 | ||
|
|
4aee179385 | ||
|
|
c599538ce1 | ||
|
|
7ffd08c41e | ||
|
|
2ee740c725 | ||
|
|
b16b9b15aa | ||
|
|
2040b453b8 | ||
|
|
18ae1822d1 | ||
|
|
1a6f2e4f9e | ||
|
|
25b2eeab14 | ||
|
|
b0030ab6f1 | ||
|
|
ae6df4fa1f | ||
|
|
7cfdd13d3a | ||
|
|
29e4f67206 | ||
|
|
a5496cd41d | ||
|
|
296fe71d19 | ||
|
|
c4f10716ac | ||
|
|
960b75a9ea | ||
|
|
2f9ce68a9e | ||
|
|
68b7ad6944 | ||
|
|
450b40a612 | ||
|
|
d4fce3a769 | ||
|
|
0f5f8fb727 | ||
|
|
c79708e550 | ||
|
|
be7470afca | ||
|
|
85cc27fed0 | ||
|
|
983061fa15 | ||
|
|
aecee2aff3 | ||
|
|
b17dfa5f58 | ||
|
|
2737127819 | ||
|
|
2e7c39965c | ||
|
|
f9c5165cb9 | ||
|
|
195ddbbba5 | ||
|
|
5960563b23 | ||
|
|
df4256cb2b | ||
|
|
1d56386708 | ||
|
|
c87a0c2bd9 | ||
|
|
cc3d6a4875 | ||
|
|
6a6cc83bc8 | ||
|
|
9f88938632 | ||
|
|
4ffff0d9d9 | ||
|
|
836f9e14e7 | ||
|
|
031c4dbdbb | ||
|
|
b278866e8c | ||
|
|
3733a657bb | ||
|
|
84434d55a0 | ||
|
|
f95d031048 | ||
|
|
8abc144cf2 | ||
|
|
9e51cd72ba | ||
|
|
3c22fe2629 | ||
|
|
cccd66c5d5 | ||
|
|
355f6d4d24 |
@@ -89,3 +89,5 @@ harness/experiments/analysis-source-s2a-windows-20260913/** -text whitespace=-bl
|
||||
|
||||
# Preserve the pinned Iced dependency bytes and upstream manifest.
|
||||
crates/vendor/iced_widget/** -text whitespace=cr-at-eol
|
||||
harness/experiments/analysis-mixed-syntax-20260923/** -text whitespace=-blank-at-eol,-blank-at-eof,cr-at-eol
|
||||
harness/experiments/state-multiselect-delivery-audit-20260923/** -text whitespace=-blank-at-eol,-blank-at-eof,cr-at-eol
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
## User input
|
||||
@@ -24,10 +24,10 @@ Keep one authoritative home for each kind of information. Use links and brief sc
|
||||
|
||||
| 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. |
|
||||
| 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. |
|
||||
| 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. |
|
||||
|
||||
@@ -38,9 +38,9 @@ Use Gitea for live coordination and the relevant code version for implementation
|
||||
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.
|
||||
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.
|
||||
|
||||
@@ -64,10 +64,10 @@ Preserve concise records of decisions and rationale, useful experiments, failed
|
||||
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
|
||||
+2
-2
@@ -17,14 +17,14 @@ Apply these criteria to the scope requested by the user:
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
If required work or verification is blocked, record the specific gap and continue independent useful work without claiming completion.
|
||||
|
||||
## Final handoff
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
## USER-OWNED APPLICATION SPECIFICATION
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
//! Read and validate a saved acceptance fixture without rewriting its source.
|
||||
//! cargo run --locked -p oa-core --example attachment_snapshot -- input.arcadia output.json
|
||||
use oa_core::{persistence, validate_candidate};
|
||||
use std::{fs::OpenOptions, io::Write, path::Path};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args: Vec<_> = std::env::args_os().skip(1).collect();
|
||||
if args.len() != 2 {
|
||||
return Err("Expected input source/JSON and a new output JSON path".into());
|
||||
}
|
||||
let project = persistence::load(Path::new(&args[0]))?;
|
||||
validate_candidate(&project)?;
|
||||
let bytes = serde_json::to_vec_pretty(&project)?;
|
||||
let mut output = OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(&args[1])?;
|
||||
output.write_all(&bytes)?;
|
||||
output.write_all(b"\n")?;
|
||||
output.sync_all()?;
|
||||
println!(
|
||||
"Validated {} objects and {} views; saved a complete Project snapshot",
|
||||
project.identities().len(),
|
||||
project.diagrams.len()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
//! Read-only saved-State geometry evidence for a predeclared native bend trial.
|
||||
//!
|
||||
//! Usage:
|
||||
//! state_bend_routing_snapshot INPUT.arcadia NEW_OUTPUT.json
|
||||
//!
|
||||
//! Loads and validates the complete Project, then independently projects/routes
|
||||
//! every saved State view through core APIs. The complete Project permits exact
|
||||
//! semantic and presentation comparison; derived geometry permits declaring a
|
||||
//! segment and insertion position before native actions. No desktop command,
|
||||
//! planner, gesture, or pin insertion helper is called. Input is never written;
|
||||
//! output uses create_new and fails rather than overwriting an existing file.
|
||||
//!
|
||||
//! Limits: this shares production core routing, so it is not an independent
|
||||
//! mathematical test of that router, nor evidence of UI picking/gesture success.
|
||||
//! It does not generate expected mutations or adapt an oracle to observed edits.
|
||||
//! The acceptance recipe must declare the intended segment/index/point and exact
|
||||
//! Project mutation before UI execution, and bind this executable/source/input
|
||||
//! and output hashes externally. Warning strings are diagnostic Debug output,
|
||||
//! not a stable interchange schema. Missing/hidden/internal transitions have no
|
||||
//! routed edge; their semantic records remain in the complete Project snapshot.
|
||||
|
||||
use oa_core::{persistence, projection, routing, validate_candidate};
|
||||
use serde_json::json;
|
||||
use std::{fs::OpenOptions, io::Write, path::Path};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args: Vec<_> = std::env::args_os().skip(1).collect();
|
||||
if args.len() != 2 {
|
||||
return Err("Expected input source/JSON and a new output JSON path".into());
|
||||
}
|
||||
let project = persistence::load(Path::new(&args[0]))?;
|
||||
validate_candidate(&project)?;
|
||||
let mut views = Vec::new();
|
||||
for (diagram_id, diagram) in &project.diagrams {
|
||||
let Some(machine) = diagram.projection.state_machine() else {
|
||||
continue;
|
||||
};
|
||||
let projected = projection::project_diagram(&project, diagram);
|
||||
let routed = routing::route_diagram(&project, diagram, &projected);
|
||||
let edges: Vec<_> = routed
|
||||
.edges
|
||||
.iter()
|
||||
.map(|(edge_id, edge)| {
|
||||
json!({
|
||||
"edge": edge_id,
|
||||
"id": edge.id,
|
||||
"points": edge.points,
|
||||
"pins": edge.pins,
|
||||
"pin_fractions": edge.pin_fractions,
|
||||
"custom_suspended": edge.custom_suspended,
|
||||
"warnings": edge.warnings.iter().map(|warning| format!("{warning:?}")).collect::<Vec<_>>(),
|
||||
"label": {
|
||||
"text": edge.label.text,
|
||||
"fraction": edge.label.fraction,
|
||||
"anchor": edge.label.anchor,
|
||||
"center": edge.label.center,
|
||||
"bounds": {
|
||||
"position": edge.label.bounds.position,
|
||||
"size": edge.label.bounds.size
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
views.push(json!({
|
||||
"diagram": diagram_id,
|
||||
"machine": machine,
|
||||
"projected": projected,
|
||||
"routed_bounds": {
|
||||
"position": routed.bounds.position,
|
||||
"size": routed.bounds.size
|
||||
},
|
||||
"routed_edges": edges
|
||||
}));
|
||||
}
|
||||
// Preserve the canonical typed Project serialization (including f32
|
||||
// formatting). Serializing it through Value directly promotes f32 to f64.
|
||||
let project_json: serde_json::Value = serde_json::from_slice(&serde_json::to_vec(&project)?)?;
|
||||
let snapshot = json!({
|
||||
"schema": "state-bend-routing-snapshot/v1",
|
||||
"project": project_json,
|
||||
"state_views": views
|
||||
});
|
||||
// Finish all reading, validation, routing and serialization before creating
|
||||
// the new evidence file. A failed load cannot leave an apparent snapshot.
|
||||
let mut bytes = serde_json::to_vec_pretty(&snapshot)?;
|
||||
bytes.push(b'\n');
|
||||
let mut output = OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(&args[1])?;
|
||||
output.write_all(&bytes)?;
|
||||
output.sync_all()?;
|
||||
println!(
|
||||
"Validated {} objects and {} saved views; snapshotted {} State views and the complete Project",
|
||||
project.identities().len(),
|
||||
project.diagrams.len(),
|
||||
views.len()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -1502,45 +1502,11 @@ pub struct Diagram {
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct EdgeRoute {
|
||||
pub edge: Id,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub source_attachment: Option<EdgeAttachment>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub target_attachment: Option<EdgeAttachment>,
|
||||
#[serde(default)]
|
||||
pub waypoints: Vec<Position>,
|
||||
#[serde(default)]
|
||||
pub label: Option<EdgeLabel>,
|
||||
}
|
||||
|
||||
/// Presentation preference on one end of a connection. Real ports take precedence.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct EdgeAttachment {
|
||||
pub side: PortAnchorSide,
|
||||
pub offset: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum EdgeEnd {
|
||||
Source,
|
||||
Target,
|
||||
}
|
||||
|
||||
impl EdgeRoute {
|
||||
pub fn attachment(&self, end: EdgeEnd) -> Option<EdgeAttachment> {
|
||||
match end {
|
||||
EdgeEnd::Source => self.source_attachment,
|
||||
EdgeEnd::Target => self.target_attachment,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_attachment(&mut self, end: EdgeEnd, attachment: Option<EdgeAttachment>) {
|
||||
match end {
|
||||
EdgeEnd::Source => self.source_attachment = attachment,
|
||||
EdgeEnd::Target => self.target_attachment = attachment,
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct EdgeLabel {
|
||||
|
||||
+38
-132
@@ -1,12 +1,7 @@
|
||||
//! Deterministic, bounded diagram geometry. Only authored pins belong in the model;
|
||||
//! callers share this derived geometry for drawing, picking, fitting and export.
|
||||
use crate::projection::{self, ProjectedEdge, ProjectedNode, ProjectedView};
|
||||
use crate::{
|
||||
Diagram, EdgeAttachment, EdgeEnd, Id, PortAnchorSide, Position, Project, Relation,
|
||||
RelationBody, RelationKind, Size,
|
||||
};
|
||||
mod attachments;
|
||||
pub use attachments::{attachment_from_point, attachment_point, semantic_port_endpoint};
|
||||
use crate::{Diagram, Id, Position, Project, Relation, RelationBody, RelationKind, Size};
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::{BTreeMap, BTreeSet, BinaryHeap};
|
||||
use std::fmt;
|
||||
@@ -46,7 +41,6 @@ pub struct RoutedEdge {
|
||||
// Derived solver state, never persisted. A preview must inherit the same
|
||||
// remaining work budget as this edge had in stable-ID settled traversal.
|
||||
search_budget_before: usize,
|
||||
attachments: [Option<EdgeAttachment>; 2],
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@@ -179,7 +173,6 @@ pub fn association_end_geometry(
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum RouteWarning {
|
||||
CoincidentAttachments,
|
||||
PinInsideObstacle { pin: usize, obstacle: Id },
|
||||
NoClearPath,
|
||||
SearchBudgetExhausted,
|
||||
@@ -188,9 +181,6 @@ pub enum RouteWarning {
|
||||
impl fmt::Display for RouteWarning {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::CoincidentAttachments => f.write_str(
|
||||
"The two attachments coincide; move either end to separate the loop tips.",
|
||||
),
|
||||
Self::PinInsideObstacle { pin, .. } => {
|
||||
write!(f, "Bend {} is inside a card or its clearance", pin + 1)
|
||||
}
|
||||
@@ -384,14 +374,7 @@ struct Endpoint {
|
||||
point: Position,
|
||||
stub: Position,
|
||||
}
|
||||
fn endpoints(
|
||||
project: &Project,
|
||||
diagram: &Diagram,
|
||||
node: &ProjectedNode,
|
||||
port: Option<Id>,
|
||||
view: &ProjectedView,
|
||||
attachment: Option<EdgeAttachment>,
|
||||
) -> Vec<Endpoint> {
|
||||
fn endpoints(node: &ProjectedNode, port: Option<Id>, view: &ProjectedView) -> Vec<Endpoint> {
|
||||
if let Some(port) = port.and_then(|id| {
|
||||
view.ports
|
||||
.iter()
|
||||
@@ -406,18 +389,26 @@ fn endpoints(
|
||||
),
|
||||
}];
|
||||
}
|
||||
if let Some(attachment) = attachment {
|
||||
return vec![attachments::endpoint(project, diagram, node, attachment)];
|
||||
}
|
||||
[
|
||||
PortAnchorSide::Left,
|
||||
PortAnchorSide::Right,
|
||||
PortAnchorSide::Top,
|
||||
PortAnchorSide::Bottom,
|
||||
let r = Rect::node(node);
|
||||
let mid = pos((r.left + r.right) / 2.0, (r.top + r.bottom) / 2.0);
|
||||
vec![
|
||||
Endpoint {
|
||||
point: pos(r.left, mid.y),
|
||||
stub: pos(r.left - PORT_STUB, mid.y),
|
||||
},
|
||||
Endpoint {
|
||||
point: pos(r.right, mid.y),
|
||||
stub: pos(r.right + PORT_STUB, mid.y),
|
||||
},
|
||||
Endpoint {
|
||||
point: pos(mid.x, r.top),
|
||||
stub: pos(mid.x, r.top - PORT_STUB),
|
||||
},
|
||||
Endpoint {
|
||||
point: pos(mid.x, r.bottom),
|
||||
stub: pos(mid.x, r.bottom + PORT_STUB),
|
||||
},
|
||||
]
|
||||
.into_iter()
|
||||
.map(|side| attachments::endpoint(project, diagram, node, EdgeAttachment { side, offset: 0.5 }))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn ancestors(id: Id, nodes: &BTreeMap<Id, &ProjectedNode>, out: &mut BTreeSet<Id>) {
|
||||
@@ -516,12 +507,6 @@ fn route_diagram_impl(
|
||||
.is_some_and(|(source, target)| source != edge.source || target != edge.target);
|
||||
let custom_suspended = promoted;
|
||||
let route = if promoted { None } else { stored };
|
||||
let attachments = [EdgeEnd::Source, EdgeEnd::Target].map(|end| {
|
||||
semantic_port_endpoint(project, edge.id, end)
|
||||
.is_none()
|
||||
.then(|| route.and_then(|r| r.attachment(end)))
|
||||
.flatten()
|
||||
});
|
||||
let pins = route.map_or_else(Vec::new, |r| r.waypoints.clone());
|
||||
let obstacles = edge_obstacles(edge, &nodes, &rectangles, &headers);
|
||||
|
||||
@@ -532,10 +517,7 @@ fn route_diagram_impl(
|
||||
});
|
||||
if let Some(cached) = cached {
|
||||
budget = cached.search_budget_before;
|
||||
if cached.pins == pins
|
||||
&& cached.attachments == attachments
|
||||
&& cached.custom_suspended == custom_suspended
|
||||
{
|
||||
if cached.pins == pins && cached.custom_suspended == custom_suspended {
|
||||
// Label-only editing cannot change the connection beneath the
|
||||
// pointer, including a settled fallback with routing warnings.
|
||||
for &point in &cached.points {
|
||||
@@ -561,22 +543,9 @@ fn route_diagram_impl(
|
||||
}
|
||||
let (points, pin_lengths, failure) = route_edge(
|
||||
edge,
|
||||
&endpoints(
|
||||
project,
|
||||
diagram,
|
||||
source,
|
||||
semantic_port_endpoint(project, edge.id, EdgeEnd::Source).or(edge.source_port),
|
||||
view,
|
||||
attachments[0],
|
||||
),
|
||||
&endpoints(
|
||||
project,
|
||||
diagram,
|
||||
target,
|
||||
semantic_port_endpoint(project, edge.id, EdgeEnd::Target).or(edge.target_port),
|
||||
view,
|
||||
attachments[1],
|
||||
),
|
||||
source,
|
||||
target,
|
||||
view,
|
||||
&pins,
|
||||
&obstacles,
|
||||
&previous,
|
||||
@@ -605,7 +574,6 @@ fn route_diagram_impl(
|
||||
warnings,
|
||||
custom_suspended,
|
||||
search_budget_before,
|
||||
attachments,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -711,46 +679,6 @@ struct RoutingSpace<'a> {
|
||||
xs: Vec<f32>,
|
||||
ys: Vec<f32>,
|
||||
}
|
||||
|
||||
fn coincident_detour(a: Endpoint, obstacles: &[Rect]) -> [Position; 3] {
|
||||
let direction = |delta: f32| if delta == 0. { 0. } else { delta.signum() };
|
||||
let normal = pos(
|
||||
direction(a.stub.x - a.point.x),
|
||||
direction(a.stub.y - a.point.y),
|
||||
);
|
||||
let tangent = pos(-normal.y, normal.x);
|
||||
let candidates: Vec<_> = [36., 72., 144., 288.]
|
||||
.into_iter()
|
||||
.flat_map(|extent| {
|
||||
[-1., 1.].into_iter().map(move |side| {
|
||||
[
|
||||
pos(a.stub.x + normal.x * extent, a.stub.y + normal.y * extent),
|
||||
pos(
|
||||
a.stub.x + (normal.x + tangent.x * side) * extent,
|
||||
a.stub.y + (normal.y + tangent.y * side) * extent,
|
||||
),
|
||||
pos(
|
||||
a.stub.x + tangent.x * side * extent,
|
||||
a.stub.y + tangent.y * side * extent,
|
||||
),
|
||||
]
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
// These are computed loop guards, not authored hard pins. Prefer a clear
|
||||
// orientation/size before asking the bounded router to connect them.
|
||||
candidates
|
||||
.iter()
|
||||
.find(|p| clear_path(&[a.stub, p[0], p[1], p[2], a.stub], obstacles))
|
||||
.or_else(|| {
|
||||
candidates.iter().find(|p| {
|
||||
p.iter()
|
||||
.all(|point| !obstacles.iter().any(|r| r.contains(*point)))
|
||||
})
|
||||
})
|
||||
.copied()
|
||||
.unwrap_or(candidates[0])
|
||||
}
|
||||
impl<'a> RoutingSpace<'a> {
|
||||
fn new(obstacles: &'a [Rect]) -> Self {
|
||||
Self {
|
||||
@@ -764,41 +692,24 @@ impl<'a> RoutingSpace<'a> {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn route_edge(
|
||||
edge: &ProjectedEdge,
|
||||
source_ends: &[Endpoint],
|
||||
target_ends: &[Endpoint],
|
||||
source: &ProjectedNode,
|
||||
target: &ProjectedNode,
|
||||
view: &ProjectedView,
|
||||
pins: &[Position],
|
||||
obstacles: &[Rect],
|
||||
previous: &[Segment],
|
||||
budget: &mut usize,
|
||||
) -> EdgePath {
|
||||
// Coincident authored tips are valid presentation, including on self loops.
|
||||
// Preserve them and add an external detour; never index an empty candidate
|
||||
// list or turn the connection into an out-and-back degenerate stub.
|
||||
let coincident = edge.source == edge.target
|
||||
&& source_ends.len() == 1
|
||||
&& target_ends.len() == 1
|
||||
&& source_ends[0].point == target_ends[0].point
|
||||
&& source_ends[0].stub == target_ends[0].stub;
|
||||
let authored_pin_count = pins.len();
|
||||
let mut loop_pins = Vec::new();
|
||||
if coincident {
|
||||
let a = source_ends[0];
|
||||
loop_pins.extend_from_slice(pins);
|
||||
loop_pins.extend(coincident_detour(a, obstacles));
|
||||
}
|
||||
let pins = if coincident { &loop_pins[..] } else { pins };
|
||||
let space = RoutingSpace::new(obstacles);
|
||||
let mut candidates = Vec::new();
|
||||
for &a in source_ends {
|
||||
for &b in target_ends {
|
||||
for a in endpoints(source, edge.source_port, view) {
|
||||
for b in endpoints(target, edge.target_port, view) {
|
||||
// A real self-relation must leave and return at distinct anchors.
|
||||
// Reusing one anchor makes the cheapest path a degenerate stub.
|
||||
if edge.source == edge.target
|
||||
&& edge.source_port.is_none()
|
||||
&& edge.target_port.is_none()
|
||||
&& a.point == b.point
|
||||
&& a.stub == b.stub
|
||||
&& !coincident
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -818,9 +729,10 @@ fn route_edge(
|
||||
// Try all side combinations cheaply before spending the bounded grid budget.
|
||||
for search in [false, true] {
|
||||
for &(estimate, a, b) in &candidates {
|
||||
if best.as_ref().is_some_and(|(cost, _, _)| {
|
||||
estimate + manhattan(a.point, a.stub) + manhattan(b.point, b.stub) > *cost
|
||||
}) {
|
||||
if best
|
||||
.as_ref()
|
||||
.is_some_and(|(cost, _, _)| estimate + 2.0 * PORT_STUB > *cost)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if obstacles
|
||||
@@ -836,7 +748,7 @@ fn route_edge(
|
||||
let mut occupied = previous.to_vec();
|
||||
record_segments(&mut occupied, &points);
|
||||
let mut pin_lengths = Vec::new();
|
||||
let mut distance = manhattan(a.point, a.stub);
|
||||
let mut distance = PORT_STUB;
|
||||
let mut start = a.stub;
|
||||
let mut failed = false;
|
||||
for (index, end) in pins
|
||||
@@ -880,20 +792,15 @@ fn route_edge(
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Some((_, points, mut pin_lengths)) = best {
|
||||
pin_lengths.truncate(authored_pin_count);
|
||||
return (
|
||||
points,
|
||||
pin_lengths,
|
||||
coincident.then_some(RouteWarning::CoincidentAttachments),
|
||||
);
|
||||
if let Some((_, points, pin_lengths)) = best {
|
||||
return (points, pin_lengths, None);
|
||||
}
|
||||
// Recoverable fallback retains every hard pin, in order, and the actual port
|
||||
// leads. Its warning is deliberately visible to consumers.
|
||||
let (_, a, b) = candidates[0];
|
||||
let mut points = vec![a.point, a.stub];
|
||||
let mut pin_lengths = Vec::new();
|
||||
let mut distance = manhattan(a.point, a.stub);
|
||||
let mut distance = PORT_STUB;
|
||||
let mut start = a.stub;
|
||||
for (index, end) in pins
|
||||
.iter()
|
||||
@@ -926,7 +833,6 @@ fn route_edge(
|
||||
start = end;
|
||||
}
|
||||
points.push(b.point);
|
||||
pin_lengths.truncate(authored_pin_count);
|
||||
(
|
||||
points,
|
||||
pin_lengths,
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
//! One boundary definition shared by persisted routing and attachment gestures.
|
||||
use super::*;
|
||||
use crate::{EdgeAttachment, EdgeEnd, Kind, PortAnchorSide};
|
||||
|
||||
/// Semantic authority, independent of whether projection currently exposes a port.
|
||||
pub fn semantic_port_endpoint(project: &Project, edge: Id, end: EdgeEnd) -> Option<Id> {
|
||||
let endpoint = if let Some(relation) = project.relations.get(&edge) {
|
||||
match (&relation.body, end) {
|
||||
(RelationBody::FunctionalExchange { source_port, .. }, EdgeEnd::Source) => *source_port,
|
||||
(RelationBody::FunctionalExchange { target_port, .. }, EdgeEnd::Target) => *target_port,
|
||||
(_, EdgeEnd::Source) => relation.source(),
|
||||
(_, EdgeEnd::Target) => relation.target(),
|
||||
}
|
||||
} else {
|
||||
let trace = project.traces.get(&edge)?;
|
||||
match end {
|
||||
EdgeEnd::Source => trace.oa,
|
||||
EdgeEnd::Target => trace.sa,
|
||||
}
|
||||
};
|
||||
project
|
||||
.elements
|
||||
.get(&endpoint)
|
||||
.filter(|e| e.kind() == Kind::Port)
|
||||
.map(|_| endpoint)
|
||||
}
|
||||
|
||||
/// Intersect the chosen cardinal scan line with the painted shape. Offset is
|
||||
/// measured over the complete projected bounding side, including its corners.
|
||||
/// Terminate's X uses its outermost stroke intersection rather than a contour.
|
||||
pub fn attachment_point(
|
||||
project: &Project,
|
||||
diagram: &Diagram,
|
||||
node: &ProjectedNode,
|
||||
attachment: &EdgeAttachment,
|
||||
) -> Position {
|
||||
let vertical = matches!(
|
||||
attachment.side,
|
||||
PortAnchorSide::Left | PortAnchorSide::Right
|
||||
);
|
||||
let (extent, depth) = if vertical {
|
||||
(node.size.height, node.size.width)
|
||||
} else {
|
||||
(node.size.width, node.size.height)
|
||||
};
|
||||
let along = attachment.offset.clamp(0., 1.) * extent;
|
||||
let t = 2. * attachment.offset.clamp(0., 1.) - 1.;
|
||||
let kind = diagram
|
||||
.projection
|
||||
.state_machine()
|
||||
.and_then(|_| project.elements.get(&node.element))
|
||||
.map(|e| e.kind());
|
||||
let inset = match kind {
|
||||
Some(Kind::Choice) => depth * 0.5 * t.abs(),
|
||||
Some(Kind::Terminate) => {
|
||||
depth
|
||||
* attachment
|
||||
.offset
|
||||
.clamp(0., 1.)
|
||||
.min(1. - attachment.offset.clamp(0., 1.))
|
||||
}
|
||||
Some(
|
||||
Kind::Initial
|
||||
| Kind::Junction
|
||||
| Kind::Final
|
||||
| Kind::ShallowHistory
|
||||
| Kind::DeepHistory
|
||||
| Kind::EntryPoint
|
||||
| Kind::ExitPoint,
|
||||
) => depth * 0.5 * (1. - (1. - t * t).max(0.).sqrt()),
|
||||
Some(Kind::Region | Kind::Fork | Kind::Join) => 0.,
|
||||
_ => {
|
||||
let radius = 10_f32.min(extent / 2.).min(depth / 2.);
|
||||
let corner = (radius - along.min(extent - along)).max(0.);
|
||||
radius - (radius * radius - corner * corner).max(0.).sqrt()
|
||||
}
|
||||
};
|
||||
match attachment.side {
|
||||
PortAnchorSide::Left => pos(node.position.x + inset, node.position.y + along),
|
||||
PortAnchorSide::Right => pos(
|
||||
node.position.x + node.size.width - inset,
|
||||
node.position.y + along,
|
||||
),
|
||||
PortAnchorSide::Top => pos(node.position.x + along, node.position.y + inset),
|
||||
PortAnchorSide::Bottom => pos(
|
||||
node.position.x + along,
|
||||
node.position.y + node.size.height - inset,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Choose a cardinal bounding side, then map its fraction onto the painted
|
||||
/// contour. Contour distances cannot distinguish the sides of a circle at its
|
||||
/// extrema: Right/0 and Top/0.5 denote the same point but different lead directions.
|
||||
/// Hysteresis is in diagram units, capped for small symbols. Only UI snapping
|
||||
/// rounds to hundredths: source/API values retain their complete precision.
|
||||
pub fn attachment_from_point(
|
||||
node: &ProjectedNode,
|
||||
cursor: Position,
|
||||
previous: Option<PortAnchorSide>,
|
||||
hysteresis: f32,
|
||||
) -> EdgeAttachment {
|
||||
let candidate = |side| EdgeAttachment {
|
||||
side,
|
||||
offset: projection::port_anchor_offset(node, side, cursor),
|
||||
};
|
||||
// Measure side intent at the box, not from a distant overshoot: Euclidean
|
||||
// distance differences converge outside a small symbol and would otherwise
|
||||
// let hysteresis retain a perpendicular side indefinitely.
|
||||
let boundary_cursor = pos(
|
||||
cursor
|
||||
.x
|
||||
.clamp(node.position.x, node.position.x + node.size.width),
|
||||
cursor
|
||||
.y
|
||||
.clamp(node.position.y, node.position.y + node.size.height),
|
||||
);
|
||||
let distance = |side| {
|
||||
length(
|
||||
projection::port_anchor_position(node, side, candidate(side).offset),
|
||||
boundary_cursor,
|
||||
)
|
||||
};
|
||||
let mut side = [
|
||||
PortAnchorSide::Left,
|
||||
PortAnchorSide::Right,
|
||||
PortAnchorSide::Top,
|
||||
PortAnchorSide::Bottom,
|
||||
]
|
||||
.into_iter()
|
||||
.min_by(|a, b| distance(*a).total_cmp(&distance(*b)))
|
||||
.unwrap();
|
||||
let hysteresis = hysteresis
|
||||
.max(0.)
|
||||
.min(node.size.width.min(node.size.height) * 0.2);
|
||||
if let Some(old) = previous
|
||||
&& distance(old) <= distance(side) + hysteresis
|
||||
{
|
||||
side = old;
|
||||
}
|
||||
let mut attachment = candidate(side);
|
||||
attachment.offset = (attachment.offset * 100.).round() / 100.;
|
||||
attachment
|
||||
}
|
||||
|
||||
pub(super) fn endpoint(
|
||||
project: &Project,
|
||||
diagram: &Diagram,
|
||||
node: &ProjectedNode,
|
||||
attachment: EdgeAttachment,
|
||||
) -> Endpoint {
|
||||
let point = attachment_point(project, diagram, node, &attachment);
|
||||
let stub = match attachment.side {
|
||||
PortAnchorSide::Left => pos(node.position.x - PORT_STUB, point.y),
|
||||
PortAnchorSide::Right => pos(node.position.x + node.size.width + PORT_STUB, point.y),
|
||||
PortAnchorSide::Top => pos(point.x, node.position.y - PORT_STUB),
|
||||
PortAnchorSide::Bottom => pos(point.x, node.position.y + node.size.height + PORT_STUB),
|
||||
};
|
||||
Endpoint { point, stub }
|
||||
}
|
||||
@@ -93,18 +93,6 @@ fn restore_serialization_skips(context: Context, fields: &mut Map<String, Value>
|
||||
.or_insert(Value::Null);
|
||||
}
|
||||
}
|
||||
Context::Diagram => {
|
||||
if let Some(routes) = fields.get_mut("routes").and_then(Value::as_array_mut) {
|
||||
for route in routes.iter_mut().filter_map(Value::as_object_mut) {
|
||||
route
|
||||
.entry("source_attachment".to_owned())
|
||||
.or_insert(Value::Null);
|
||||
route
|
||||
.entry("target_attachment".to_owned())
|
||||
.or_insert(Value::Null);
|
||||
}
|
||||
}
|
||||
}
|
||||
Context::Node => {
|
||||
fields.entry("port_anchors".to_owned()).or_insert(json!([]));
|
||||
}
|
||||
@@ -256,12 +244,7 @@ fn compact_nested(context: Context, fields: &mut Map<String, Value>) {
|
||||
if let Some(routes) = fields.get_mut("routes").and_then(Value::as_array_mut) {
|
||||
for route in routes {
|
||||
if let Some(route) = route.as_object_mut() {
|
||||
omit_defaults(
|
||||
route,
|
||||
&object(
|
||||
json!({"waypoints": [], "label": null, "source_attachment": null, "target_attachment": null}),
|
||||
),
|
||||
);
|
||||
omit_defaults(route, &object(json!({"waypoints": [], "label": null})));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -643,20 +643,6 @@ pub fn validate(p: &Project) -> Vec<Issue> {
|
||||
}
|
||||
let mut routed_edges = BTreeSet::new();
|
||||
for (index, route) in d.routes.iter().enumerate() {
|
||||
for attachment in [route.source_attachment, route.target_attachment]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
{
|
||||
if !attachment.offset.is_finite() || !(0.0..=1.0).contains(&attachment.offset) {
|
||||
error(
|
||||
&mut out,
|
||||
d.id,
|
||||
"invalid_attachment_offset",
|
||||
"routes",
|
||||
"Connection attachment offset must be finite and between zero and one.",
|
||||
);
|
||||
}
|
||||
}
|
||||
if !routed_edges.insert(route.edge) {
|
||||
error(
|
||||
&mut out,
|
||||
|
||||
@@ -1,385 +0,0 @@
|
||||
use oa_core::{projection::*, routing::*, *};
|
||||
|
||||
fn pos(x: f32, y: f32) -> Position {
|
||||
Position { x, y }
|
||||
}
|
||||
fn anchor(side: PortAnchorSide, offset: f32) -> EdgeAttachment {
|
||||
EdgeAttachment { side, offset }
|
||||
}
|
||||
fn fixture() -> (Project, Id, Id, Id, Id) {
|
||||
let mut p = Project::empty();
|
||||
let a = p
|
||||
.add_element(Kind::Activity, Layer::Oa, "Report", None)
|
||||
.unwrap();
|
||||
let b = p
|
||||
.add_element(Kind::Activity, Layer::Oa, "Receive", None)
|
||||
.unwrap();
|
||||
let edge = p.relate(RelationKind::Interaction, "Report status", a, b);
|
||||
let d = p.ensure_diagram(Layer::Oa);
|
||||
p.place(d, a, pos(50., 50.));
|
||||
p.place(d, b, pos(750., 200.));
|
||||
(p, d, a, b, edge)
|
||||
}
|
||||
fn stored(edge: Id) -> EdgeRoute {
|
||||
EdgeRoute {
|
||||
edge,
|
||||
source_attachment: None,
|
||||
target_attachment: None,
|
||||
waypoints: vec![],
|
||||
label: None,
|
||||
}
|
||||
}
|
||||
fn routed(p: &Project, d: Id) -> RoutedView {
|
||||
let diagram = &p.diagrams[&d];
|
||||
route_diagram(p, diagram, &project_diagram(p, diagram))
|
||||
}
|
||||
fn near(a: f32, b: f32) {
|
||||
assert!((a - b).abs() < 0.002, "{a} != {b}");
|
||||
}
|
||||
fn orthogonal(points: &[Position]) {
|
||||
assert!(points.len() >= 2);
|
||||
for span in points.windows(2) {
|
||||
assert!(span[0].x == span[1].x || span[0].y == span[1].y, "{span:?}");
|
||||
}
|
||||
}
|
||||
fn length(points: &[Position]) -> f32 {
|
||||
points
|
||||
.windows(2)
|
||||
.map(|p| (p[0].x - p[1].x).abs() + (p[0].y - p[1].y).abs())
|
||||
.sum()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authored_boundary_follows_projected_owner_and_preview_matches_committed_geometry() {
|
||||
let (mut p, d, a, _, edge) = fixture();
|
||||
let initial = routed(&p, d);
|
||||
let mut route = stored(edge);
|
||||
route.source_attachment = Some(anchor(PortAnchorSide::Bottom, 0.25));
|
||||
route.target_attachment = Some(anchor(PortAnchorSide::Top, 0.75));
|
||||
route.waypoints = vec![pos(460., 350.)];
|
||||
p.diagrams.get_mut(&d).unwrap().routes.push(route.clone());
|
||||
let view = project_diagram(&p, &p.diagrams[&d]);
|
||||
let preview = route_edge_preview(&p, &p.diagrams[&d], &view, &initial, edge);
|
||||
let full = routed(&p, d);
|
||||
assert_eq!(
|
||||
preview, full,
|
||||
"attachment-only changes must invalidate the label-only path cache"
|
||||
);
|
||||
let n = view.nodes.iter().find(|n| n.element == a).unwrap();
|
||||
assert_eq!(
|
||||
full.edges[&edge].points[0],
|
||||
pos(
|
||||
n.position.x + n.size.width * 0.25,
|
||||
n.position.y + n.size.height
|
||||
)
|
||||
);
|
||||
assert_eq!(full.edges[&edge].pins, route.waypoints);
|
||||
orthogonal(&full.edges[&edge].points);
|
||||
let node = p
|
||||
.diagrams
|
||||
.get_mut(&d)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.values_mut()
|
||||
.find(|n| n.element == a)
|
||||
.unwrap();
|
||||
node.position.x += 75.;
|
||||
node.size.width += 100.;
|
||||
node.size.height += 50.;
|
||||
let moved = routed(&p, d);
|
||||
near(
|
||||
moved.edges[&edge].points[0].x - full.edges[&edge].points[0].x,
|
||||
100.,
|
||||
);
|
||||
near(
|
||||
moved.edges[&edge].points[0].y - full.edges[&edge].points[0].y,
|
||||
50.,
|
||||
);
|
||||
assert_eq!(p.diagrams[&d].routes[0], route);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attachment_only_preview_cannot_reuse_old_endpoints_with_unchanged_pins() {
|
||||
let (mut p, d, _, _, edge) = fixture();
|
||||
let baseline = routed(&p, d);
|
||||
let mut route = stored(edge);
|
||||
route.source_attachment = Some(anchor(PortAnchorSide::Top, 0.2));
|
||||
p.diagrams.get_mut(&d).unwrap().routes.push(route);
|
||||
let view = project_diagram(&p, &p.diagrams[&d]);
|
||||
let preview = route_edge_preview(&p, &p.diagrams[&d], &view, &baseline, edge);
|
||||
assert_ne!(preview.edges[&edge].points, baseline.edges[&edge].points);
|
||||
assert_eq!(preview, routed(&p, d));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ports_remain_authoritative_even_when_projection_filters_the_port() {
|
||||
let mut p = Project::empty();
|
||||
let a = p
|
||||
.add_element(Kind::Function, Layer::Sa, "Send", None)
|
||||
.unwrap();
|
||||
let b = p
|
||||
.add_element(Kind::Function, Layer::Sa, "Receive", None)
|
||||
.unwrap();
|
||||
let edge = p.relate(RelationKind::FunctionalExchange, "Data", a, b);
|
||||
let d = p.ensure_diagram(Layer::Sa);
|
||||
p.place(d, a, pos(0., 0.));
|
||||
p.place(d, b, pos(600., 0.));
|
||||
let diagram = p.diagrams[&d].clone();
|
||||
let view = project_diagram(&p, &diagram);
|
||||
let expected = route_diagram(&p, &diagram, &view);
|
||||
assert!(semantic_port_endpoint(&p, edge, EdgeEnd::Source).is_some());
|
||||
let mut route = stored(edge);
|
||||
route.source_attachment = Some(anchor(PortAnchorSide::Bottom, 0.1));
|
||||
route.target_attachment = Some(anchor(PortAnchorSide::Top, 0.9));
|
||||
p.diagrams.get_mut(&d).unwrap().routes.push(route);
|
||||
assert_eq!(routed(&p, d), expected);
|
||||
let mut hidden = view.clone();
|
||||
hidden.ports.clear();
|
||||
for e in &mut hidden.edges {
|
||||
e.source_port = None;
|
||||
e.target_port = None;
|
||||
}
|
||||
let absent = route_diagram(&p, &diagram, &hidden);
|
||||
assert_eq!(
|
||||
route_diagram(&p, &p.diagrams[&d], &hidden),
|
||||
absent,
|
||||
"hidden ports must not activate a dormant node preference"
|
||||
);
|
||||
}
|
||||
|
||||
fn state(kind: Kind) -> (Project, Id, Id, Id) {
|
||||
let mut p = Project::empty();
|
||||
let owner = p
|
||||
.add_element(Kind::Entity, Layer::Oa, "Crew", None)
|
||||
.unwrap();
|
||||
let machine = p
|
||||
.add_element(Kind::StateMachine, Layer::Oa, "Modes", Some(owner))
|
||||
.unwrap();
|
||||
let region = p
|
||||
.add_element(Kind::Region, Layer::Oa, "Main", Some(machine))
|
||||
.unwrap();
|
||||
let a = p.add_element(kind, Layer::Oa, "A", Some(region)).unwrap();
|
||||
let b = p
|
||||
.add_element(Kind::State, Layer::Oa, "B", Some(region))
|
||||
.unwrap();
|
||||
let edge = p.relate(RelationKind::StateTransition, "Next", a, b);
|
||||
let d = p.ensure_diagram(Layer::Oa);
|
||||
p.diagrams.get_mut(&d).unwrap().projection = ProjectionKind::StateMachine { machine };
|
||||
p.place(d, a, pos(100., 100.));
|
||||
p.place(d, b, pos(600., 100.));
|
||||
(p, d, a, edge)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nonrectangular_state_attachments_intersect_the_painted_shape() {
|
||||
for kind in [
|
||||
Kind::Initial,
|
||||
Kind::Junction,
|
||||
Kind::Final,
|
||||
Kind::Choice,
|
||||
Kind::ShallowHistory,
|
||||
Kind::DeepHistory,
|
||||
Kind::EntryPoint,
|
||||
Kind::ExitPoint,
|
||||
Kind::Terminate,
|
||||
Kind::Fork,
|
||||
Kind::Join,
|
||||
Kind::State,
|
||||
] {
|
||||
let (p, d, a, _) = state(kind);
|
||||
let diagram = &p.diagrams[&d];
|
||||
let view = project_diagram(&p, diagram);
|
||||
let n = view.nodes.iter().find(|n| n.element == a).unwrap();
|
||||
for side in [
|
||||
PortAnchorSide::Left,
|
||||
PortAnchorSide::Right,
|
||||
PortAnchorSide::Top,
|
||||
PortAnchorSide::Bottom,
|
||||
] {
|
||||
for offset in [0., 0.2, 0.5, 0.8, 1.] {
|
||||
let at = attachment_point(&p, diagram, n, &anchor(side, offset));
|
||||
let x = (at.x - n.position.x) / n.size.width;
|
||||
let y = (at.y - n.position.y) / n.size.height;
|
||||
match kind {
|
||||
Kind::Choice => near((2. * x - 1.).abs() + (2. * y - 1.).abs(), 1.),
|
||||
Kind::Terminate => near((x - y).abs().min((x + y - 1.).abs()), 0.),
|
||||
Kind::Fork | Kind::Join => near(x.min(1. - x).min(y.min(1. - y)), 0.),
|
||||
Kind::State => {
|
||||
assert!((0. ..=1.).contains(&x) && (0. ..=1.).contains(&y));
|
||||
if offset == 0. || offset == 1. {
|
||||
assert!(
|
||||
x != 0. && x != 1. || y != 0. && y != 1.,
|
||||
"rounded corner cannot use bounding-box corner"
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => near((2. * x - 1.).powi(2) + (2. * y - 1.).powi(2), 1.),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inset_contour_leads_keep_pin_fractions_and_labels_on_the_complete_path() {
|
||||
let (mut p, d, _, edge) = state(Kind::Choice);
|
||||
let mut route = stored(edge);
|
||||
route.source_attachment = Some(anchor(PortAnchorSide::Right, 0.));
|
||||
route.waypoints = vec![pos(380., 40.)];
|
||||
route.label = Some(EdgeLabel {
|
||||
fraction: 0.3,
|
||||
offset: pos(0., 0.),
|
||||
});
|
||||
p.diagrams.get_mut(&d).unwrap().routes.push(route);
|
||||
let result = routed(&p, d);
|
||||
let r = &result.edges[&edge];
|
||||
orthogonal(&r.points);
|
||||
assert!(r.warnings.is_empty(), "{:?}", r.warnings);
|
||||
let pin = r.pins[0];
|
||||
let pin_index = r.points.iter().position(|p| *p == pin).unwrap();
|
||||
near(
|
||||
r.pin_fractions[0],
|
||||
length(&r.points[..=pin_index]) / length(&r.points),
|
||||
);
|
||||
assert_eq!(
|
||||
r.label.anchor,
|
||||
point_along_polyline(&r.points, 0.3).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn small_symbol_drag_chooses_the_intended_exit_side_at_low_zoom() {
|
||||
for kind in [
|
||||
Kind::Initial,
|
||||
Kind::Choice,
|
||||
Kind::Terminate,
|
||||
Kind::Fork,
|
||||
Kind::State,
|
||||
] {
|
||||
let (p, d, a, _) = state(kind);
|
||||
let view = project_diagram(&p, &p.diagrams[&d]);
|
||||
let n = view.nodes.iter().find(|n| n.element == a).unwrap();
|
||||
for zoom in [0.5, 1., 2.] {
|
||||
for overshoot in [0., 20.] {
|
||||
let cursor = pos(n.position.x + n.size.width / 2., n.position.y - overshoot);
|
||||
let chosen =
|
||||
attachment_from_point(n, cursor, Some(PortAnchorSide::Right), 6. / zoom);
|
||||
assert_eq!(
|
||||
chosen,
|
||||
anchor(PortAnchorSide::Top, 0.5),
|
||||
"{kind:?}, zoom {zoom}, overshoot {overshoot}"
|
||||
);
|
||||
let painted = attachment_point(&p, &p.diagrams[&d], n, &chosen);
|
||||
near(painted.x, n.position.x + n.size.width / 2.);
|
||||
near(
|
||||
painted.y,
|
||||
n.position.y
|
||||
+ if kind == Kind::Terminate {
|
||||
n.size.height / 2.
|
||||
} else {
|
||||
0.
|
||||
},
|
||||
);
|
||||
}
|
||||
let near_corner = pos(n.position.x + n.size.width - 1., n.position.y + 1.);
|
||||
let stable =
|
||||
attachment_from_point(n, near_corner, Some(PortAnchorSide::Right), 6. / zoom);
|
||||
assert_eq!(
|
||||
stable.side,
|
||||
PortAnchorSide::Right,
|
||||
"corner hysteresis stays stable"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coincident_self_attachments_retain_hard_pins_and_form_a_visible_loop() {
|
||||
let (mut p, d, a, edge) = state(Kind::State);
|
||||
if let RelationBody::StateTransition { target, .. } =
|
||||
&mut p.relations.get_mut(&edge).unwrap().body
|
||||
{
|
||||
*target = a;
|
||||
}
|
||||
for pins in [vec![], vec![pos(400., 30.)]] {
|
||||
let mut route = stored(edge);
|
||||
route.source_attachment = Some(anchor(PortAnchorSide::Top, 0.5));
|
||||
route.target_attachment = route.source_attachment;
|
||||
route.waypoints = pins.clone();
|
||||
p.diagrams.get_mut(&d).unwrap().routes = vec![route.clone()];
|
||||
let result = routed(&p, d);
|
||||
let r = &result.edges[&edge];
|
||||
orthogonal(&r.points);
|
||||
assert_eq!(r.points.first(), r.points.last());
|
||||
assert!(length(&r.points) > 2. * PORT_STUB + 70.);
|
||||
assert_eq!(r.pins, pins);
|
||||
assert_eq!(r.pin_fractions.len(), pins.len());
|
||||
assert!(
|
||||
r.warnings.contains(&RouteWarning::CoincidentAttachments),
|
||||
"{:?}",
|
||||
r.warnings
|
||||
);
|
||||
assert_eq!(routed(&p, d), result, "deterministic fallback");
|
||||
assert_eq!(p.diagrams[&d].routes[0], route);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coincident_loop_chooses_clear_side_instead_of_inventing_an_obstructed_pin() {
|
||||
for blocker_side in [-1., 1.] {
|
||||
let (mut p, d, a, edge) = state(Kind::State);
|
||||
if let RelationBody::StateTransition { target, .. } =
|
||||
&mut p.relations.get_mut(&edge).unwrap().body
|
||||
{
|
||||
*target = a;
|
||||
}
|
||||
let mut route = stored(edge);
|
||||
route.source_attachment = Some(anchor(PortAnchorSide::Top, 0.5));
|
||||
route.target_attachment = route.source_attachment;
|
||||
p.diagrams.get_mut(&d).unwrap().routes.push(route);
|
||||
let view = project_diagram(&p, &p.diagrams[&d]);
|
||||
let source = view.nodes.iter().find(|n| n.element == a).unwrap();
|
||||
let tip = attachment_point(
|
||||
&p,
|
||||
&p.diagrams[&d],
|
||||
source,
|
||||
&anchor(PortAnchorSide::Top, 0.5),
|
||||
);
|
||||
let region = p.elements[&a].parent;
|
||||
let blocker = p
|
||||
.add_element(Kind::State, Layer::Oa, "Detour obstruction", region)
|
||||
.unwrap();
|
||||
let left = if blocker_side > 0. {
|
||||
tip.x + 30.
|
||||
} else {
|
||||
tip.x - 30. - 224.
|
||||
};
|
||||
p.place(d, blocker, pos(left, tip.y - 144.));
|
||||
let result = routed(&p, d);
|
||||
let r = &result.edges[&edge];
|
||||
assert_eq!(r.warnings, vec![RouteWarning::CoincidentAttachments]);
|
||||
orthogonal(&r.points);
|
||||
let view = project_diagram(&p, &p.diagrams[&d]);
|
||||
let n = view.nodes.iter().find(|n| n.element == blocker).unwrap();
|
||||
for segment in r.points.windows(2) {
|
||||
for step in 0..=100 {
|
||||
let t = step as f32 / 100.;
|
||||
let q = pos(
|
||||
segment[0].x + (segment[1].x - segment[0].x) * t,
|
||||
segment[0].y + (segment[1].y - segment[0].y) * t,
|
||||
);
|
||||
assert!(
|
||||
!(q.x > n.position.x - CLEARANCE
|
||||
&& q.x < n.position.x + n.size.width + CLEARANCE
|
||||
&& q.y > n.position.y - CLEARANCE
|
||||
&& q.y < n.position.y + n.size.height + CLEARANCE),
|
||||
"route crosses blocker: {q:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
r.pins.is_empty(),
|
||||
"generated detours must never become authored pins"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,272 +0,0 @@
|
||||
use oa_core::{language, projection::*, routing::*, *};
|
||||
|
||||
fn pos(x: f32, y: f32) -> Position {
|
||||
Position { x, y }
|
||||
}
|
||||
|
||||
fn id(project: &Project, alias: &str) -> Id {
|
||||
project
|
||||
.identities()
|
||||
.into_iter()
|
||||
.find(|(_, a)| *a == alias)
|
||||
.unwrap()
|
||||
.0
|
||||
}
|
||||
|
||||
fn authored(edge: Id) -> EdgeRoute {
|
||||
EdgeRoute {
|
||||
edge,
|
||||
source_attachment: Some(EdgeAttachment {
|
||||
side: PortAnchorSide::Bottom,
|
||||
offset: 0.2,
|
||||
}),
|
||||
target_attachment: Some(EdgeAttachment {
|
||||
side: PortAnchorSide::Top,
|
||||
offset: 0.8,
|
||||
}),
|
||||
waypoints: vec![pos(520., 420.)],
|
||||
label: Some(EdgeLabel {
|
||||
fraction: 0.35,
|
||||
offset: pos(17., -23.),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn routed(project: &Project, diagram: Id) -> RoutedView {
|
||||
let d = &project.diagrams[&diagram];
|
||||
route_diagram(project, d, &project_diagram(project, d))
|
||||
}
|
||||
|
||||
fn fixture() -> (Project, Id, Id, Id, Id, Id, Id) {
|
||||
let mut p = language::parse(
|
||||
r#"arcadia 7;
|
||||
project lifecycle "Attachment lifecycle" {
|
||||
oa_activity group "Group" { oa_activity first "First" {} }
|
||||
oa_activity second "Second" {}
|
||||
oa_activity replacement "Replacement" {}
|
||||
interaction signal "Signal" { source @first; target @second; }
|
||||
diagram view "View" {}
|
||||
diagram other "Other" {}
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
let (d, other, group, first, second, replacement, edge) = (
|
||||
id(&p, "view"),
|
||||
id(&p, "other"),
|
||||
id(&p, "group"),
|
||||
id(&p, "first"),
|
||||
id(&p, "second"),
|
||||
id(&p, "replacement"),
|
||||
id(&p, "signal"),
|
||||
);
|
||||
for diagram in [d, other] {
|
||||
p.place(diagram, group, pos(20., 20.));
|
||||
p.place(diagram, first, pos(100., 130.));
|
||||
p.place(diagram, second, pos(750., 150.));
|
||||
p.place(diagram, replacement, pos(750., 570.));
|
||||
p.diagrams
|
||||
.get_mut(&diagram)
|
||||
.unwrap()
|
||||
.routes
|
||||
.push(authored(edge));
|
||||
}
|
||||
(p, d, other, group, first, replacement, edge)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attachment_fold_promotion_suspends_the_complete_route_and_expand_restores_it() {
|
||||
let (mut p, d, other, group, _, _, edge) = fixture();
|
||||
assert_eq!(semantic_port_endpoint(&p, edge, EdgeEnd::Source), None);
|
||||
assert_eq!(semantic_port_endpoint(&p, edge, EdgeEnd::Target), None);
|
||||
let before = p.clone();
|
||||
let expanded = routed(&p, d);
|
||||
let other_geometry = routed(&p, other);
|
||||
let parent = p.diagrams[&d]
|
||||
.nodes
|
||||
.values()
|
||||
.find(|n| n.element == group)
|
||||
.unwrap()
|
||||
.id;
|
||||
p.diagrams
|
||||
.get_mut(&d)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&parent)
|
||||
.unwrap()
|
||||
.collapsed = true;
|
||||
let folded = routed(&p, d);
|
||||
assert!(folded.edges[&edge].custom_suspended);
|
||||
assert_eq!(folded.edges[&edge].pins, []);
|
||||
assert!(
|
||||
folded.edges[&edge]
|
||||
.warnings
|
||||
.contains(&RouteWarning::PromotedEndpoints)
|
||||
);
|
||||
assert_eq!(p.diagrams[&d].routes, before.diagrams[&d].routes);
|
||||
let mut automatic = p.clone();
|
||||
automatic.diagrams.get_mut(&d).unwrap().routes.clear();
|
||||
assert_eq!(
|
||||
folded,
|
||||
routed(&automatic, d),
|
||||
"attachments, pins and label must all be dormant on promotion"
|
||||
);
|
||||
assert_eq!(p.diagrams[&other], before.diagrams[&other]);
|
||||
assert_eq!(routed(&p, other), other_geometry);
|
||||
p.diagrams
|
||||
.get_mut(&d)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&parent)
|
||||
.unwrap()
|
||||
.collapsed = false;
|
||||
assert_eq!(p, before);
|
||||
assert_eq!(routed(&p, d), expanded);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attachment_reconnect_retains_slot_preferences_and_other_view_records_with_exact_history() {
|
||||
let (p, d, other, _, _, replacement, edge) = fixture();
|
||||
let before = routed(&p, d);
|
||||
let mut relation = p.relations[&edge].clone();
|
||||
let RelationBody::Interaction { target, .. } = &mut relation.body else {
|
||||
unreachable!()
|
||||
};
|
||||
*target = replacement;
|
||||
let mut expected = p.clone();
|
||||
expected.relations.insert(edge, relation.clone());
|
||||
let mut session = Session::new(p.clone());
|
||||
session
|
||||
.execute(Command::EditRelation(edge, relation))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
session.project(),
|
||||
&expected,
|
||||
"reconnect changes only the semantic endpoint; all per-view records remain exact"
|
||||
);
|
||||
assert_eq!(session.project().diagrams[&other], p.diagrams[&other]);
|
||||
for diagram in [d, other] {
|
||||
let current = session.project();
|
||||
let view = project_diagram(current, ¤t.diagrams[&diagram]);
|
||||
let node = view
|
||||
.nodes
|
||||
.iter()
|
||||
.find(|n| n.element == replacement)
|
||||
.unwrap();
|
||||
let preference = current.diagrams[&diagram].routes[0]
|
||||
.target_attachment
|
||||
.unwrap();
|
||||
let expected_tip =
|
||||
attachment_point(current, ¤t.diagrams[&diagram], node, &preference);
|
||||
let result = routed(current, diagram);
|
||||
assert_eq!(result.edges[&edge].points.last(), Some(&expected_tip));
|
||||
assert_eq!(
|
||||
result.edges[&edge].pins,
|
||||
p.diagrams[&diagram].routes[0].waypoints
|
||||
);
|
||||
}
|
||||
assert_ne!(
|
||||
routed(session.project(), d).edges[&edge].points.last(),
|
||||
before.edges[&edge].points.last()
|
||||
);
|
||||
session.undo();
|
||||
assert_eq!(session.project(), &p);
|
||||
assert_eq!(routed(session.project(), d), before);
|
||||
session.redo();
|
||||
assert_eq!(session.project(), &expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attachment_direct_port_endpoints_remain_authoritative_when_their_glyphs_are_filtered() {
|
||||
let mut p = Project::empty();
|
||||
let system = p
|
||||
.add_element(Kind::System, Layer::Sa, "System", None)
|
||||
.unwrap();
|
||||
let actor = p
|
||||
.add_element(Kind::SystemActor, Layer::Sa, "Actor", None)
|
||||
.unwrap();
|
||||
let function = p
|
||||
.add_element(Kind::Function, Layer::Sa, "Send", None)
|
||||
.unwrap();
|
||||
let mut ports = Vec::new();
|
||||
for (owner, name, contract) in [
|
||||
(
|
||||
system,
|
||||
"Output",
|
||||
PortContract::Component {
|
||||
kind: ComponentPortKind::Flow,
|
||||
orientation: ComponentPortOrientation::Out,
|
||||
},
|
||||
),
|
||||
(
|
||||
actor,
|
||||
"Input",
|
||||
PortContract::Component {
|
||||
kind: ComponentPortKind::Flow,
|
||||
orientation: ComponentPortOrientation::In,
|
||||
},
|
||||
),
|
||||
(
|
||||
function,
|
||||
"Function output",
|
||||
PortContract::FunctionOutput {
|
||||
outgoing_items: vec![],
|
||||
},
|
||||
),
|
||||
] {
|
||||
let port = Id::new_v4();
|
||||
p.elements
|
||||
.insert(port, make_port(&p, port, owner, name, contract).unwrap());
|
||||
ports.push(port);
|
||||
}
|
||||
p.relate(
|
||||
RelationKind::Allocation,
|
||||
"Function allocation",
|
||||
system,
|
||||
function,
|
||||
);
|
||||
let boundary = p.relate(
|
||||
RelationKind::BoundaryExchange,
|
||||
"Boundary",
|
||||
ports[0],
|
||||
ports[1],
|
||||
);
|
||||
let allocation = p.relate(
|
||||
RelationKind::PortAllocation,
|
||||
"Port allocation",
|
||||
ports[0],
|
||||
ports[2],
|
||||
);
|
||||
let d = p.ensure_diagram(Layer::Sa);
|
||||
p.place(d, system, pos(0., 0.));
|
||||
p.place(d, actor, pos(700., 0.));
|
||||
p.place(d, function, pos(0., 450.));
|
||||
let original = p.clone();
|
||||
let baseline = routed(&p, d);
|
||||
for (edge, target) in [(boundary, ports[1]), (allocation, ports[2])] {
|
||||
assert!(baseline.edges.contains_key(&edge));
|
||||
assert_eq!(
|
||||
semantic_port_endpoint(&p, edge, EdgeEnd::Source),
|
||||
Some(ports[0])
|
||||
);
|
||||
assert_eq!(
|
||||
semantic_port_endpoint(&p, edge, EdgeEnd::Target),
|
||||
Some(target)
|
||||
);
|
||||
let mut route = authored(edge);
|
||||
route.waypoints.clear();
|
||||
route.label = None;
|
||||
p.diagrams.get_mut(&d).unwrap().routes.push(route);
|
||||
}
|
||||
assert_eq!(routed(&p, d), baseline);
|
||||
let mut hidden = project_diagram(&p, &p.diagrams[&d]);
|
||||
hidden.ports.clear();
|
||||
for edge in &mut hidden.edges {
|
||||
edge.source_port = None;
|
||||
edge.target_port = None;
|
||||
}
|
||||
assert_eq!(
|
||||
route_diagram(&p, &p.diagrams[&d], &hidden),
|
||||
route_diagram(&original, &original.diagrams[&d], &hidden),
|
||||
"filtered direct Port endpoints must not activate their stored preferences"
|
||||
);
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
use oa_core::{projection::*, routing::*, *};
|
||||
|
||||
#[test]
|
||||
fn valid_item_trace_follows_manual_port_anchors_and_ignores_dormant_attachments() {
|
||||
let mut project = Project::empty();
|
||||
let oa = project
|
||||
.add_element(Kind::Activity, Layer::Oa, "Send", None)
|
||||
.unwrap();
|
||||
let sa = project
|
||||
.add_element(Kind::Function, Layer::Sa, "Receive", None)
|
||||
.unwrap();
|
||||
let ports = [(oa, Layer::Oa), (sa, Layer::Sa)].map(|(owner, layer)| {
|
||||
let id = Id::new_v4();
|
||||
project.elements.insert(
|
||||
id,
|
||||
Element {
|
||||
id,
|
||||
alias: format!("port_{}", id.simple()),
|
||||
name: "Status port".into(),
|
||||
description: "Existing legacy port retained by Item trace".into(),
|
||||
parent: Some(owner),
|
||||
body: ElementBody::shared_in(
|
||||
layer,
|
||||
SharedBody::Port {
|
||||
contract: PortContract::Legacy,
|
||||
legacy: Some(LegacyPortContract {
|
||||
direction: PortDirection::InOut,
|
||||
interface: None,
|
||||
}),
|
||||
},
|
||||
),
|
||||
},
|
||||
);
|
||||
id
|
||||
});
|
||||
let trace = Trace {
|
||||
id: Id::new_v4(),
|
||||
alias: "status_correspondence".into(),
|
||||
name: "Status correspondence".into(),
|
||||
description: "Preserve this semantic evidence".into(),
|
||||
oa: ports[0],
|
||||
sa: ports[1],
|
||||
kind: TraceKind::Item,
|
||||
baseline: None,
|
||||
};
|
||||
project.traces.insert(trace.id, trace.clone());
|
||||
let diagram = project.ensure_diagram(Layer::Oa);
|
||||
project.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::Traceability;
|
||||
let owners = [
|
||||
project.place(diagram, oa, Position { x: 100., y: 100. }),
|
||||
project.place(diagram, sa, Position { x: 800., y: 300. }),
|
||||
];
|
||||
validate_candidate(&project).unwrap();
|
||||
let semantic_elements = project.elements.clone();
|
||||
let semantic_relations = project.relations.clone();
|
||||
let semantic_traces = project.traces.clone();
|
||||
let view = project_diagram(&project, &project.diagrams[&diagram]);
|
||||
let edge = view.edges.iter().find(|edge| edge.id == trace.id).unwrap();
|
||||
assert!(edge.trace);
|
||||
assert_eq!((edge.source, edge.target), (owners[0], owners[1]));
|
||||
assert_eq!((edge.source_port, edge.target_port), (None, None));
|
||||
let initial = route_diagram(&project, &project.diagrams[&diagram], &view);
|
||||
for (index, (side, offset)) in [(PortAnchorSide::Top, 0.25), (PortAnchorSide::Bottom, 0.75)]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
project
|
||||
.diagrams
|
||||
.get_mut(&diagram)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&owners[index])
|
||||
.unwrap()
|
||||
.port_anchors
|
||||
.push(PortAnchor {
|
||||
port: ports[index],
|
||||
side,
|
||||
offset,
|
||||
});
|
||||
}
|
||||
validate_candidate(&project).unwrap();
|
||||
let moved_view = project_diagram(&project, &project.diagrams[&diagram]);
|
||||
let moved = route_diagram(&project, &project.diagrams[&diagram], &moved_view);
|
||||
for index in 0..2 {
|
||||
let marker = moved_view
|
||||
.ports
|
||||
.iter()
|
||||
.find(|port| port.element == ports[index] && port.owner == owners[index])
|
||||
.unwrap();
|
||||
let points = &moved.edges[&trace.id].points;
|
||||
let tip = if index == 0 {
|
||||
points[0]
|
||||
} else {
|
||||
*points.last().unwrap()
|
||||
};
|
||||
assert_eq!(tip, marker.position);
|
||||
let old = &initial.edges[&trace.id].points;
|
||||
assert_ne!(
|
||||
tip,
|
||||
if index == 0 {
|
||||
old[0]
|
||||
} else {
|
||||
*old.last().unwrap()
|
||||
}
|
||||
);
|
||||
}
|
||||
project
|
||||
.diagrams
|
||||
.get_mut(&diagram)
|
||||
.unwrap()
|
||||
.routes
|
||||
.push(EdgeRoute {
|
||||
edge: trace.id,
|
||||
waypoints: vec![],
|
||||
label: None,
|
||||
source_attachment: Some(EdgeAttachment {
|
||||
side: PortAnchorSide::Bottom,
|
||||
offset: 0.9,
|
||||
}),
|
||||
target_attachment: Some(EdgeAttachment {
|
||||
side: PortAnchorSide::Top,
|
||||
offset: 0.1,
|
||||
}),
|
||||
});
|
||||
validate_candidate(&project).unwrap();
|
||||
let dormant_view = project_diagram(&project, &project.diagrams[&diagram]);
|
||||
assert_eq!(dormant_view, moved_view);
|
||||
assert_eq!(
|
||||
route_diagram(&project, &project.diagrams[&diagram], &dormant_view),
|
||||
moved
|
||||
);
|
||||
assert_eq!(project.elements, semantic_elements);
|
||||
assert_eq!(project.relations, semantic_relations);
|
||||
assert_eq!(project.traces, semantic_traces);
|
||||
assert_eq!(project.traces[&trace.id], trace);
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
//! Route the exact native acceptance fixture and intended saved checkpoints.
|
||||
use oa_core::{projection::project_diagram, routing::route_diagram, *};
|
||||
|
||||
const FIXTURE: &str =
|
||||
include_str!("../../../harness/experiments/edge-attachments-20260922/native-fixture.arcadia");
|
||||
|
||||
fn id(project: &Project, alias: &str) -> Id {
|
||||
project
|
||||
.identities()
|
||||
.into_iter()
|
||||
.find(|(_, name)| *name == alias)
|
||||
.unwrap()
|
||||
.0
|
||||
}
|
||||
|
||||
fn set(
|
||||
project: &mut Project,
|
||||
view: &str,
|
||||
edge: &str,
|
||||
end: EdgeEnd,
|
||||
attachment: Option<(PortAnchorSide, f32)>,
|
||||
) {
|
||||
let view = id(project, view);
|
||||
let edge = id(project, edge);
|
||||
let route = project
|
||||
.diagrams
|
||||
.get_mut(&view)
|
||||
.unwrap()
|
||||
.routes
|
||||
.iter_mut()
|
||||
.find(|route| route.edge == edge)
|
||||
.unwrap();
|
||||
route.set_attachment(
|
||||
end,
|
||||
attachment.map(|(side, offset)| EdgeAttachment { side, offset }),
|
||||
);
|
||||
}
|
||||
|
||||
fn clear(project: &Project, checkpoint: &str) {
|
||||
validate_candidate(project).unwrap();
|
||||
for (view, edges) in [
|
||||
("dispatch_view", &["dispatch_request"][..]),
|
||||
("readiness_view", &["activate", "initialize", "finish"][..]),
|
||||
] {
|
||||
let diagram = &project.diagrams[&id(project, view)];
|
||||
let routed = route_diagram(project, diagram, &project_diagram(project, diagram));
|
||||
for alias in edges {
|
||||
let edge = &routed.edges[&id(project, alias)];
|
||||
assert!(
|
||||
edge.warnings.is_empty(),
|
||||
"{checkpoint}/{view}/{alias}: {:?}",
|
||||
edge.warnings
|
||||
);
|
||||
let stored = diagram
|
||||
.routes
|
||||
.iter()
|
||||
.find(|r| r.edge == id(project, alias))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
edge.pins, stored.waypoints,
|
||||
"{checkpoint}/{alias}: manual pins retained"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_workshop_baseline_and_planned_attachments_have_clear_routes() {
|
||||
use EdgeEnd::{Source, Target};
|
||||
use PortAnchorSide::{Bottom, Top};
|
||||
let mut project = language::parse(FIXTURE).unwrap();
|
||||
let baseline = project.clone();
|
||||
clear(&project, "B");
|
||||
set(
|
||||
&mut project,
|
||||
"dispatch_view",
|
||||
"dispatch_request",
|
||||
Source,
|
||||
Some((Top, 0.25)),
|
||||
);
|
||||
clear(&project, "O1");
|
||||
set(
|
||||
&mut project,
|
||||
"dispatch_view",
|
||||
"dispatch_request",
|
||||
Target,
|
||||
Some((Bottom, 0.75)),
|
||||
);
|
||||
clear(&project, "O2");
|
||||
set(
|
||||
&mut project,
|
||||
"dispatch_view",
|
||||
"dispatch_request",
|
||||
Source,
|
||||
None,
|
||||
);
|
||||
clear(&project, "O3");
|
||||
set(
|
||||
&mut project,
|
||||
"readiness_view",
|
||||
"activate",
|
||||
Source,
|
||||
Some((Top, 0.25)),
|
||||
);
|
||||
clear(&project, "S1");
|
||||
set(
|
||||
&mut project,
|
||||
"readiness_view",
|
||||
"activate",
|
||||
Target,
|
||||
Some((Bottom, 0.75)),
|
||||
);
|
||||
clear(&project, "S2");
|
||||
set(&mut project, "readiness_view", "activate", Target, None);
|
||||
clear(&project, "S3");
|
||||
set(
|
||||
&mut project,
|
||||
"readiness_view",
|
||||
"initialize",
|
||||
Source,
|
||||
Some((Top, 0.5)),
|
||||
);
|
||||
clear(&project, "S4");
|
||||
set(
|
||||
&mut project,
|
||||
"readiness_view",
|
||||
"finish",
|
||||
Source,
|
||||
Some((Top, 0.5)),
|
||||
);
|
||||
clear(&project, "S5");
|
||||
assert_eq!(project.elements, baseline.elements);
|
||||
assert_eq!(project.relations, baseline.relations);
|
||||
for alias in ["dispatch_reference", "readiness_reference", "ports_view"] {
|
||||
let view = id(&project, alias);
|
||||
assert_eq!(project.diagrams[&view], baseline.diagrams[&view]);
|
||||
}
|
||||
for alias in ["dispatch_view", "readiness_view"] {
|
||||
let view = id(&project, alias);
|
||||
assert_eq!(
|
||||
project.diagrams[&view].nodes,
|
||||
baseline.diagrams[&view].nodes
|
||||
);
|
||||
for route in &project.diagrams[&view].routes {
|
||||
let original = baseline.diagrams[&view]
|
||||
.routes
|
||||
.iter()
|
||||
.find(|r| r.edge == route.edge)
|
||||
.unwrap();
|
||||
assert_eq!(route.waypoints, original.waypoints);
|
||||
assert_eq!(route.label, original.label);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,290 +0,0 @@
|
||||
use oa_core::{language, persistence, source_project::*, *};
|
||||
use serde_json::{Value, json};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
const SOURCE: &str = r#"arcadia 7;
|
||||
project attachments "Connection ends" id "70000000-0000-4000-8000-000000000001" {
|
||||
oa_activity first "First" id "70000000-0000-4000-8000-000000000002" {}
|
||||
oa_activity second "Second" id "70000000-0000-4000-8000-000000000003" {}
|
||||
interaction signal "Signal" id "70000000-0000-4000-8000-000000000004" { source @first; target @second; }
|
||||
diagram view "View" id "70000000-0000-4000-8000-000000000005" {
|
||||
routes [{ edge @signal; waypoints [{ x 120; y 80; }];
|
||||
label { fraction 0.5; offset { x 3; y -7; }; };
|
||||
source_attachment { side Left; offset 0.25; };
|
||||
target_attachment { side Bottom; offset 1; };
|
||||
}];
|
||||
}
|
||||
diagram other "Other" id "70000000-0000-4000-8000-000000000006" { routes [{ edge @signal; }]; }
|
||||
}
|
||||
"#;
|
||||
|
||||
fn id(project: &Project, alias: &str) -> Id {
|
||||
project
|
||||
.identities()
|
||||
.into_iter()
|
||||
.find(|(_, a)| *a == alias)
|
||||
.unwrap()
|
||||
.0
|
||||
}
|
||||
|
||||
fn original() -> Project {
|
||||
language::parse(SOURCE).unwrap()
|
||||
}
|
||||
|
||||
fn set(diagram: Id, route: EdgeRoute) -> Command {
|
||||
Command::SetEdgeRoute { diagram, route }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edge_attachments_roundtrip_source_json_bundle_and_automatic_defaults() {
|
||||
let project = original();
|
||||
let view = id(&project, "view");
|
||||
let route = &project.diagrams[&view].routes[0];
|
||||
assert_eq!(
|
||||
route.source_attachment,
|
||||
Some(EdgeAttachment {
|
||||
side: PortAnchorSide::Left,
|
||||
offset: 0.25
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
route.target_attachment,
|
||||
Some(EdgeAttachment {
|
||||
side: PortAnchorSide::Bottom,
|
||||
offset: 1.0
|
||||
})
|
||||
);
|
||||
for text in [SOURCE.to_owned(), language::format(&project)] {
|
||||
assert_eq!(language::parse(&text).unwrap(), project);
|
||||
}
|
||||
assert_eq!(
|
||||
persistence::decode(&serde_json::to_vec(&project).unwrap()).unwrap(),
|
||||
project
|
||||
);
|
||||
assert_eq!(
|
||||
compile_bundle(&format_bundle(&project), ProjectLimits::default())
|
||||
.unwrap()
|
||||
.decoded
|
||||
.project,
|
||||
project
|
||||
);
|
||||
|
||||
let automatic = &project.diagrams[&id(&project, "other")].routes[0];
|
||||
assert_eq!(automatic.source_attachment, None);
|
||||
assert_eq!(automatic.target_attachment, None);
|
||||
let encoded = serde_json::to_value(automatic).unwrap();
|
||||
assert!(encoded.get("source_attachment").is_none());
|
||||
assert!(encoded.get("target_attachment").is_none());
|
||||
let mut with_null = encoded.clone();
|
||||
with_null["source_attachment"] = Value::Null;
|
||||
with_null["target_attachment"] = Value::Null;
|
||||
assert_eq!(
|
||||
serde_json::from_value::<EdgeRoute>(with_null).unwrap(),
|
||||
*automatic
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_value::<EdgeRoute>(encoded).unwrap(),
|
||||
*automatic
|
||||
);
|
||||
|
||||
// Old source without either new field still means automatic presentation.
|
||||
let old = SOURCE
|
||||
.replace(" source_attachment { side Left; offset 0.25; };\n", "")
|
||||
.replace(" target_attachment { side Bottom; offset 1; };\n", "");
|
||||
let old = language::parse(&old).unwrap();
|
||||
assert!(
|
||||
old.diagrams
|
||||
.values()
|
||||
.flat_map(|d| &d.routes)
|
||||
.all(|r| r.source_attachment.is_none() && r.target_attachment.is_none())
|
||||
);
|
||||
assert!(!language::format(&old).contains("_attachment"));
|
||||
assert_eq!(
|
||||
persistence::decode(&serde_json::to_vec(&old).unwrap()).unwrap(),
|
||||
old
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edge_attachments_reject_malformed_nested_source_and_json_without_defaults() {
|
||||
let project = original();
|
||||
let edge = id(&project, "signal");
|
||||
for bad in [
|
||||
json!({}),
|
||||
json!({"side":"Top"}),
|
||||
json!({"offset":0.5}),
|
||||
json!({"side":"Diagonal","offset":0.5}),
|
||||
json!({"side":"Top","offset":null}),
|
||||
json!({"side":"Top","offset":0.5,"unknown":true}),
|
||||
json!({"side":"Top","offset":"NaN"}),
|
||||
] {
|
||||
for field in ["source_attachment", "target_attachment"] {
|
||||
let mut route = json!({"edge":edge});
|
||||
route[field] = bad.clone();
|
||||
assert!(
|
||||
serde_json::from_value::<EdgeRoute>(route).is_err(),
|
||||
"{field}: {bad}"
|
||||
);
|
||||
}
|
||||
}
|
||||
for bad in [
|
||||
"{}",
|
||||
"{ side Top; }",
|
||||
"{ offset 0.5; }",
|
||||
"{ side Diagonal; offset 0.5; }",
|
||||
"{ side Top; offset null; }",
|
||||
"{ side Top; offset 0.5; unknown true; }",
|
||||
"{ side Top; offset -0.01; }",
|
||||
"{ side Top; offset 1.01; }",
|
||||
"{ side Top; offset NaN; }",
|
||||
"{ side Top; offset 1e999; }",
|
||||
] {
|
||||
let source = SOURCE.replace("{ side Left; offset 0.25; }", bad);
|
||||
assert!(language::parse(&source).is_err(), "{bad}");
|
||||
let bundle = SourceBundle {
|
||||
entry: "p.arcadia".into(),
|
||||
files: BTreeMap::from([("p.arcadia".into(), source)]),
|
||||
};
|
||||
assert!(
|
||||
compile_bundle(&bundle, ProjectLimits::default()).is_err(),
|
||||
"{bad}"
|
||||
);
|
||||
}
|
||||
for version in [1, 2] {
|
||||
let legacy =
|
||||
format!("arcadia {version}; project p \"P\" {{ diagram v \"V\" {{ routes []; }} }}");
|
||||
assert!(
|
||||
language::parse(&legacy).is_err(),
|
||||
"legacy route extension must not be silently admitted"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edge_attachments_nonfinite_and_out_of_range_commands_reject_atomically() {
|
||||
let project = original();
|
||||
let view = id(&project, "view");
|
||||
let first = id(&project, "first");
|
||||
for offset in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, -0.01, 1.01] {
|
||||
for end in [EdgeEnd::Source, EdgeEnd::Target] {
|
||||
let mut route = project.diagrams[&view].routes[0].clone();
|
||||
route.set_attachment(
|
||||
end,
|
||||
Some(EdgeAttachment {
|
||||
side: PortAnchorSide::Top,
|
||||
offset,
|
||||
}),
|
||||
);
|
||||
let mut session = Session::new(project.clone());
|
||||
assert!(
|
||||
session
|
||||
.execute(Command::Batch(vec![
|
||||
Command::Rename(first, "Must roll back".into()),
|
||||
set(view, route)
|
||||
]))
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(session.project(), &project);
|
||||
assert!(!session.can_undo() && !session.can_redo());
|
||||
}
|
||||
}
|
||||
for offset in [-0.01, 1.01] {
|
||||
let mut value = serde_json::to_value(&project).unwrap();
|
||||
value["diagrams"][view.to_string()]["routes"][0]["source_attachment"]["offset"] =
|
||||
json!(offset);
|
||||
assert!(persistence::decode(&serde_json::to_vec(&value).unwrap()).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edge_attachments_reset_one_end_and_all_routing_have_exact_history_and_view_scope() {
|
||||
let project = original();
|
||||
let view = id(&project, "view");
|
||||
let edge = id(&project, "signal");
|
||||
let mut session = Session::new(project.clone());
|
||||
let mut route = project.diagrams[&view].routes[0].clone();
|
||||
route.source_attachment = Some(EdgeAttachment {
|
||||
side: PortAnchorSide::Right,
|
||||
offset: 0.75,
|
||||
});
|
||||
let command = set(view, route.clone());
|
||||
assert_eq!(
|
||||
serde_json::from_value::<Command>(serde_json::to_value(&command).unwrap()).unwrap(),
|
||||
command
|
||||
);
|
||||
session.execute(command).unwrap();
|
||||
let mut expected = project.clone();
|
||||
expected.diagrams.get_mut(&view).unwrap().routes[0] = route.clone();
|
||||
assert_eq!(
|
||||
session.project(),
|
||||
&expected,
|
||||
"only this view/end changes; semantic fields and other view stay exact"
|
||||
);
|
||||
session.undo();
|
||||
assert_eq!(session.project(), &project);
|
||||
assert!(!session.can_undo() && session.can_redo());
|
||||
session.redo();
|
||||
assert_eq!(session.project(), &expected);
|
||||
route.source_attachment = None;
|
||||
session.execute(set(view, route.clone())).unwrap();
|
||||
let mut reset = expected.clone();
|
||||
reset.diagrams.get_mut(&view).unwrap().routes[0] = route;
|
||||
assert_eq!(
|
||||
session.project(),
|
||||
&reset,
|
||||
"opposite attachment, pins and label survive endpoint reset"
|
||||
);
|
||||
session.undo();
|
||||
assert_eq!(session.project(), &expected);
|
||||
session
|
||||
.execute(Command::RemoveEdgeRoute {
|
||||
diagram: view,
|
||||
edge,
|
||||
})
|
||||
.unwrap();
|
||||
let mut removed = expected.clone();
|
||||
removed.diagrams.get_mut(&view).unwrap().routes.clear();
|
||||
assert_eq!(session.project(), &removed);
|
||||
session.undo();
|
||||
assert_eq!(session.project(), &expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edge_attachments_nested_writeback_preserves_comments_adjacent_tokens_and_other_files() {
|
||||
let current = compile_bundle(&SourceBundle {
|
||||
entry: "p.arcadia".into(),
|
||||
files: BTreeMap::from([
|
||||
("p.arcadia".into(), "// project rationale\narcadia 7; project p \"P\" { include \"model.arcadia\"; include \"views.arcadia\"; }\n".into()),
|
||||
("model.arcadia".into(), "// untouched semantic ownership\noa_activity a \"A\" {}\noa_activity b \"B\" {}\ninteraction signal \"Signal\" { source @a; target @b; }\n".into()),
|
||||
("views.arcadia".into(), "// view rationale\r\ndiagram view \"View\" {\r\n routes [ { edge @signal; // endpoint explanation\r\n source_attachment { side Left; offset 0.25 ; // exact nested rationale\r\n }; target_attachment { side Bottom; offset 1; };\r\n waypoints [{ x 120; y 80; }]; label { fraction 0.5; offset { x 3; y -7; }; }; } ];\r\n}\r\ndiagram other \"Other\" { routes [{ edge @signal; }]; } // adjacent view\r\n".into()),
|
||||
]),
|
||||
}, ProjectLimits::default()).unwrap();
|
||||
let mut project = current.decoded.project.clone();
|
||||
let view = id(&project, "view");
|
||||
project.diagrams.get_mut(&view).unwrap().routes[0]
|
||||
.source_attachment
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.offset = 0.375;
|
||||
let changed = update_bundle(¤t, &project, ProjectLimits::default()).unwrap();
|
||||
assert_eq!(changed.decoded.project, project);
|
||||
for path in ["p.arcadia", "model.arcadia"] {
|
||||
assert_eq!(changed.bundle.files[path], current.bundle.files[path]);
|
||||
}
|
||||
assert_eq!(
|
||||
changed.bundle.files["views.arcadia"],
|
||||
current.bundle.files["views.arcadia"].replace("offset 0.25 ;", "offset 0.375 ;")
|
||||
);
|
||||
assert_eq!(
|
||||
compile_bundle(&changed.bundle, ProjectLimits::default())
|
||||
.unwrap()
|
||||
.bundle,
|
||||
changed.bundle
|
||||
);
|
||||
assert_eq!(
|
||||
update_bundle(&changed, &project, ProjectLimits::default())
|
||||
.unwrap()
|
||||
.bundle,
|
||||
changed.bundle
|
||||
);
|
||||
}
|
||||
@@ -441,8 +441,6 @@ fn nested_owner_bounds_group_movement_and_authored_routes_share_the_same_geometr
|
||||
diagram: d,
|
||||
route: EdgeRoute {
|
||||
edge,
|
||||
source_attachment: None,
|
||||
target_attachment: None,
|
||||
waypoints: vec![pos(500., 420.)],
|
||||
label: None,
|
||||
},
|
||||
|
||||
@@ -70,8 +70,6 @@ fn fixture() -> Fixture {
|
||||
fn route(edge: Id) -> EdgeRoute {
|
||||
EdgeRoute {
|
||||
edge,
|
||||
source_attachment: None,
|
||||
target_attachment: None,
|
||||
waypoints: vec![Position { x: 550., y: 240. }, Position { x: 550., y: 360. }],
|
||||
label: Some(EdgeLabel {
|
||||
fraction: 0.4,
|
||||
@@ -691,8 +689,6 @@ fn old_models_get_empty_routes_and_route_json_is_strict() {
|
||||
serde_json::from_value::<EdgeRoute>(minimal).unwrap(),
|
||||
EdgeRoute {
|
||||
edge: f.edge,
|
||||
source_attachment: None,
|
||||
target_attachment: None,
|
||||
waypoints: vec![],
|
||||
label: None
|
||||
}
|
||||
|
||||
@@ -27,8 +27,6 @@ fn route(p: &Project, d: Id) -> RoutedView {
|
||||
fn set_route(p: &mut Project, d: Id, edge: Id, pins: Vec<Position>, label: Option<EdgeLabel>) {
|
||||
p.diagrams.get_mut(&d).unwrap().routes.push(EdgeRoute {
|
||||
edge,
|
||||
source_attachment: None,
|
||||
target_attachment: None,
|
||||
waypoints: pins,
|
||||
label,
|
||||
});
|
||||
|
||||
@@ -117,8 +117,6 @@ fn authored_overlap_is_exact_and_reset_only_repositions_automatic_text() {
|
||||
.routes
|
||||
.push(EdgeRoute {
|
||||
edge: fe,
|
||||
source_attachment: None,
|
||||
target_attachment: None,
|
||||
waypoints: vec![],
|
||||
label: Some(authored.clone()),
|
||||
});
|
||||
|
||||
@@ -301,8 +301,6 @@ fn folding_retains_authored_routes_without_false_self_transitions() {
|
||||
let pins = vec![point(690., 480.), point(690., 230.)];
|
||||
f.p.diagrams.get_mut(&f.d).unwrap().routes.push(EdgeRoute {
|
||||
edge: out,
|
||||
source_attachment: None,
|
||||
target_attachment: None,
|
||||
waypoints: pins.clone(),
|
||||
label: None,
|
||||
});
|
||||
@@ -364,15 +362,11 @@ fn grouped_moves_translate_hidden_descendants_and_pins_only_in_one_view() {
|
||||
let pins = vec![point(570., 460.)];
|
||||
f.p.diagrams.get_mut(&f.d).unwrap().routes.push(EdgeRoute {
|
||||
edge: f.local,
|
||||
source_attachment: None,
|
||||
target_attachment: None,
|
||||
waypoints: pins.clone(),
|
||||
label: Some(label.clone()),
|
||||
});
|
||||
f.p.diagrams.get_mut(&f.d).unwrap().routes.push(EdgeRoute {
|
||||
edge: f.external,
|
||||
source_attachment: None,
|
||||
target_attachment: None,
|
||||
waypoints: vec![point(690., 230.)],
|
||||
label: None,
|
||||
});
|
||||
@@ -428,8 +422,6 @@ fn cross_machine_reparent_requires_atomic_placement_and_route_repair() {
|
||||
let placement = f.p.place(f.d, orphan, point(30., 750.));
|
||||
f.p.diagrams.get_mut(&f.d).unwrap().routes.push(EdgeRoute {
|
||||
edge,
|
||||
source_attachment: None,
|
||||
target_attachment: None,
|
||||
waypoints: vec![],
|
||||
label: None,
|
||||
});
|
||||
@@ -464,8 +456,6 @@ fn cross_machine_reparent_requires_atomic_placement_and_route_repair() {
|
||||
diagram: other,
|
||||
route: EdgeRoute {
|
||||
edge,
|
||||
source_attachment: None,
|
||||
target_attachment: None,
|
||||
waypoints: vec![],
|
||||
label: None,
|
||||
},
|
||||
@@ -593,8 +583,6 @@ fn layout_reserves_internal_rows_and_state_routes_retain_hard_pins() {
|
||||
let pins = vec![point(730., 130.), point(1060., 130.)];
|
||||
f.p.diagrams.get_mut(&f.d).unwrap().routes.push(EdgeRoute {
|
||||
edge: f.self_edge,
|
||||
source_attachment: None,
|
||||
target_attachment: None,
|
||||
waypoints: pins.clone(),
|
||||
label: Some(EdgeLabel {
|
||||
fraction: 0.25,
|
||||
@@ -664,8 +652,6 @@ fn moving_a_region_does_not_translate_pins_attached_to_a_stationary_state_bounda
|
||||
let pins = vec![point(280., 390.), point(280., 470.)];
|
||||
f.p.diagrams.get_mut(&f.d).unwrap().routes.push(EdgeRoute {
|
||||
edge: f.local,
|
||||
source_attachment: None,
|
||||
target_attachment: None,
|
||||
waypoints: pins.clone(),
|
||||
label: None,
|
||||
});
|
||||
@@ -744,8 +730,6 @@ fn initial_transition_attaches_to_the_compact_symbol_and_preview_keeps_settled_g
|
||||
);
|
||||
f.p.diagrams.get_mut(&f.d).unwrap().routes.push(EdgeRoute {
|
||||
edge: f.self_edge,
|
||||
source_attachment: None,
|
||||
target_attachment: None,
|
||||
waypoints: vec![],
|
||||
label: Some(EdgeLabel {
|
||||
fraction: 0.3,
|
||||
|
||||
@@ -748,8 +748,6 @@ fn allocation_artifact_and_manual_trace_cascades_are_named_and_refused() {
|
||||
fn route(edge: Id) -> EdgeRoute {
|
||||
EdgeRoute {
|
||||
edge,
|
||||
source_attachment: None,
|
||||
target_attachment: None,
|
||||
waypoints: vec![xy(420., 400.), xy(420., 550.)],
|
||||
label: Some(EdgeLabel {
|
||||
fraction: 0.4,
|
||||
|
||||
@@ -70,7 +70,6 @@ impl Root<'_> {
|
||||
pan: self.app.pan,
|
||||
context: self.app.route_context().unwrap(),
|
||||
selected_bend: None,
|
||||
selected_attachment: None,
|
||||
editable: false,
|
||||
placing: false,
|
||||
connection: None,
|
||||
|
||||
+13
-130
@@ -18,12 +18,12 @@ mod hosted;
|
||||
mod reference_authoring;
|
||||
#[cfg(test)]
|
||||
mod review_navigation_tests;
|
||||
#[cfg(test)]
|
||||
mod source_indentation_tests;
|
||||
pub(crate) mod source_rename;
|
||||
mod source_symbols;
|
||||
pub(crate) mod text_workspace;
|
||||
#[cfg(test)]
|
||||
mod traceability_navigation_tests;
|
||||
#[cfg(test)]
|
||||
mod transition_context_tests;
|
||||
#[cfg(test)]
|
||||
mod view_copy_tests;
|
||||
@@ -361,7 +361,6 @@ pub enum Message {
|
||||
Page(Page),
|
||||
Guide(crate::guide::Event),
|
||||
Traceability(crate::views::traceability::Event),
|
||||
ReviewLayerTraces,
|
||||
Behavior(crate::behavior::Event),
|
||||
Layer(Layer),
|
||||
Select(Id),
|
||||
@@ -430,6 +429,7 @@ pub enum Message {
|
||||
Pan(iced::Vector),
|
||||
Fit,
|
||||
SourceEdit(text_editor::Action),
|
||||
SourceIndent(crate::source_navigation::SymbolContext, bool),
|
||||
ApplySource,
|
||||
RevertSource,
|
||||
SourceUndo,
|
||||
@@ -496,7 +496,6 @@ pub struct App {
|
||||
pub camera_epoch: u64,
|
||||
camera_boundary_epoch: u64,
|
||||
pub selected_bend: Option<(Id, usize)>,
|
||||
pub selected_attachment: Option<(crate::route_edit::Context, Id, EdgeEnd)>,
|
||||
pub exact_strings: bool,
|
||||
pub document: Document,
|
||||
pub page: Page,
|
||||
@@ -617,7 +616,6 @@ impl App {
|
||||
camera_epoch: 0,
|
||||
camera_boundary_epoch: 0,
|
||||
selected_bend: None,
|
||||
selected_attachment: None,
|
||||
exact_strings: false,
|
||||
document,
|
||||
page: Page::Diagram,
|
||||
@@ -1108,9 +1106,6 @@ impl App {
|
||||
previous != (self.layer, self.active_diagram)
|
||||
}
|
||||
fn select(&mut self, id: Id) {
|
||||
if id != self.selected {
|
||||
self.selected_attachment = None;
|
||||
}
|
||||
if self.selected == id {
|
||||
return;
|
||||
}
|
||||
@@ -2313,30 +2308,7 @@ impl App {
|
||||
"Apply or revert pending edits before using modeling actions.".into();
|
||||
return Task::none();
|
||||
}
|
||||
// Show acts on the diagram captured by the validated menu.
|
||||
// Ordinary cross-layer selection would switch to that element's
|
||||
// preferred same-layer view before the placement command runs.
|
||||
let show_in_mixed_view = matches!(&*action, Message::PlaceSelected)
|
||||
&& self
|
||||
.active_diagram
|
||||
.and_then(|id| self.project().diagrams.get(&id))
|
||||
.is_some_and(|d| {
|
||||
d.projection == ProjectionKind::Traceability
|
||||
&& self
|
||||
.project()
|
||||
.elements
|
||||
.get(&menu.request.object)
|
||||
.is_some_and(|e| {
|
||||
e.kind() != Kind::Port && d.projection.includes(e.kind())
|
||||
})
|
||||
});
|
||||
if show_in_mixed_view {
|
||||
self.selected = menu.request.object;
|
||||
self.selected_attachment = None;
|
||||
self.refresh_draft();
|
||||
} else {
|
||||
self.select(menu.request.object);
|
||||
}
|
||||
self.select(menu.request.object);
|
||||
self.selected_node = menu.request.placement;
|
||||
self.selected_bend = menu.request.bend.map(|i| (menu.request.object, i));
|
||||
return self.update(*action);
|
||||
@@ -2360,15 +2332,6 @@ impl App {
|
||||
} else if self.behavior.creation.has_pending() {
|
||||
self.behavior.creation.acknowledge_commit();
|
||||
self.status = "Behavior creation cancelled".into();
|
||||
} else if self.page == Page::Behavior
|
||||
&& matches!(
|
||||
self.behavior.diagram.tool,
|
||||
behavior::state_edit::Tool::AdjustAttachment { .. }
|
||||
)
|
||||
{
|
||||
// Menu arming invalidates canvas keyboard ownership. Escape
|
||||
// must also cancel before the next canvas click grants it.
|
||||
self.behavior.diagram.tool = behavior::state_edit::Tool::Select;
|
||||
}
|
||||
if self.connection.take().is_some() {
|
||||
self.status = "Drawing cancelled · Select / move is active".into();
|
||||
@@ -2534,23 +2497,6 @@ impl App {
|
||||
iced::widget::scrollable::RelativeOffset::START,
|
||||
);
|
||||
}
|
||||
Message::ReviewLayerTraces => {
|
||||
// Reuse Page navigation so modal, placement and behavior-form
|
||||
// guards run before changing the transient inventory filter.
|
||||
let navigation = self.update(Message::Page(Page::Traces));
|
||||
if self.page == Page::Traces && !self.behavior.has_pending() {
|
||||
self.traceability.filter = crate::views::traceability::Filter::Traces;
|
||||
self.traceability.query.clear();
|
||||
return Task::batch([
|
||||
navigation,
|
||||
iced::widget::operation::snap_to(
|
||||
"traceability-list",
|
||||
iced::widget::scrollable::RelativeOffset::START,
|
||||
),
|
||||
]);
|
||||
}
|
||||
return navigation;
|
||||
}
|
||||
Message::Search(value) => {
|
||||
self.search = value;
|
||||
self.explorer_page = 0;
|
||||
@@ -3371,10 +3317,16 @@ impl App {
|
||||
self.pan += delta;
|
||||
}
|
||||
Message::Fit => self.fit(),
|
||||
Message::SourceEdit(action) => {
|
||||
Message::SourceEdit(_) | Message::SourceIndent(..) => {
|
||||
self.source_jump = None;
|
||||
let before = self.source_editor.text();
|
||||
self.source_editor.perform(action);
|
||||
match message {
|
||||
Message::SourceEdit(action) => self.source_editor.perform(action),
|
||||
Message::SourceIndent(_, outdent) => {
|
||||
crate::source_indentation::apply(&mut self.source_editor, outdent);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
let after = self.source_editor.text();
|
||||
if after != before {
|
||||
self.source_navigation.refresh(&after);
|
||||
@@ -4580,7 +4532,7 @@ impl App {
|
||||
}
|
||||
fn edit_route(&mut self, event: crate::route_edit::Event) {
|
||||
use crate::route_edit::{Event, insert_pin, stored_route};
|
||||
if self.connection.is_some() || self.page != Page::Diagram {
|
||||
if self.connection.is_some() {
|
||||
return;
|
||||
}
|
||||
let Some(context) = self.route_context() else {
|
||||
@@ -4608,75 +4560,6 @@ impl App {
|
||||
.filter(|(id, _)| *id == self.selected)
|
||||
.map(|(_, i)| i);
|
||||
let command = match event {
|
||||
Event::ChooseAttachment(end) => {
|
||||
if crate::route_edit::attachment_node(
|
||||
self.project(),
|
||||
diagram,
|
||||
&view,
|
||||
self.selected,
|
||||
end,
|
||||
)
|
||||
.is_some()
|
||||
{
|
||||
self.selected_attachment = Some((context, self.selected, end));
|
||||
self.selected_bend = None;
|
||||
self.status = format!(
|
||||
"Drag the {} attachment handle along its element boundary · Esc cancels",
|
||||
if end == EdgeEnd::Source {
|
||||
"source"
|
||||
} else {
|
||||
"target"
|
||||
}
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
Event::ResetAttachment(end) => {
|
||||
if crate::route_edit::attachment_node(
|
||||
self.project(),
|
||||
diagram,
|
||||
&view,
|
||||
self.selected,
|
||||
end,
|
||||
)
|
||||
.is_none()
|
||||
|| route.attachment(end).is_none()
|
||||
{
|
||||
return;
|
||||
}
|
||||
route.set_attachment(end, None);
|
||||
selected_bend = None;
|
||||
Command::SetEdgeRoute {
|
||||
diagram: context.diagram,
|
||||
route,
|
||||
}
|
||||
}
|
||||
Event::CommitAttachment(expected, changed, end, owner) => {
|
||||
if expected != context
|
||||
|| changed.edge != self.selected
|
||||
|| crate::route_edit::attachment_node(
|
||||
self.project(),
|
||||
diagram,
|
||||
&view,
|
||||
self.selected,
|
||||
end,
|
||||
)
|
||||
.is_none_or(|node| node.id != owner)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// A boundary gesture owns exactly one presentation field.
|
||||
let mut intended = route.clone();
|
||||
intended.set_attachment(end, changed.attachment(end));
|
||||
if changed != intended || intended == route {
|
||||
return;
|
||||
}
|
||||
selected_bend = None;
|
||||
Command::SetEdgeRoute {
|
||||
diagram: context.diagram,
|
||||
route: intended,
|
||||
}
|
||||
}
|
||||
Event::SelectBend(id, index) => {
|
||||
if id == self.selected && index < route.waypoints.len() {
|
||||
self.selected_bend = Some((id, index));
|
||||
|
||||
@@ -69,7 +69,6 @@ fn canvas(app: &App) -> DiagramCanvas<'_> {
|
||||
pan: app.pan,
|
||||
context: app.route_context().unwrap(),
|
||||
selected_bend: None,
|
||||
selected_attachment: None,
|
||||
editable: !app.draft_dirty() && !app.source_dirty(),
|
||||
placing: false,
|
||||
connection: None,
|
||||
|
||||
@@ -301,17 +301,6 @@ impl App {
|
||||
_ => return Task::none(),
|
||||
};
|
||||
let placing = point.is_none() && matches!(intent, Intent::Create { .. });
|
||||
let filter = if matches!(intent, Intent::Existing { .. })
|
||||
&& self
|
||||
.project()
|
||||
.diagrams
|
||||
.get(&diagram)
|
||||
.is_some_and(|d| d.projection == ProjectionKind::Traceability)
|
||||
{
|
||||
Filter::AllCompatible
|
||||
} else {
|
||||
Filter::Context
|
||||
};
|
||||
let saved = Saved {
|
||||
version: 1,
|
||||
base: oa_core::transitions::project_fingerprint(self.project()),
|
||||
@@ -320,7 +309,7 @@ impl App {
|
||||
point,
|
||||
placing,
|
||||
query: String::new(),
|
||||
filter,
|
||||
filter: Filter::Context,
|
||||
show_placed: false,
|
||||
page: 0,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use super::*;
|
||||
mod traceability;
|
||||
|
||||
fn setup(layer: Layer) -> App {
|
||||
let mut app = App::with_project(Project::empty());
|
||||
@@ -76,7 +75,6 @@ fn architecture_f2_routes_selected_node_to_inline_rename_and_exact_undo() {
|
||||
pan: app.pan,
|
||||
context: app.route_context().unwrap(),
|
||||
selected_bend: None,
|
||||
selected_attachment: None,
|
||||
editable: true,
|
||||
placing: false,
|
||||
connection: None,
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn traceability_existing_defaults_show_opposite_layer_and_place_only_one_node() {
|
||||
let mut project = Project::empty();
|
||||
let sa = project
|
||||
.add_element(Kind::Function, Layer::Sa, "Deliver report", None)
|
||||
.unwrap();
|
||||
let system = project
|
||||
.add_element(Kind::System, Layer::Sa, "System", None)
|
||||
.unwrap();
|
||||
let diagram = project.ensure_diagram(Layer::Oa);
|
||||
project.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::Traceability;
|
||||
let mut app = App::with_project(project);
|
||||
app.page = Page::Diagram;
|
||||
app.layer = Layer::Oa;
|
||||
app.active_diagram = Some(diagram);
|
||||
let before = app.document.snapshot();
|
||||
send(&mut app, Event::Existing(None));
|
||||
assert_eq!(
|
||||
app.architecture.as_ref().unwrap().saved.filter,
|
||||
Filter::AllCompatible
|
||||
);
|
||||
let listed = candidates(&app);
|
||||
assert!(listed.iter().any(|(id, _, shown)| *id == sa && !shown));
|
||||
assert!(listed.iter().any(|(id, _, shown)| *id == system && !shown));
|
||||
send(&mut app, Event::Pick(sa));
|
||||
assert!(app.architecture.as_ref().unwrap().saved.placing);
|
||||
assert_eq!(app.project(), &before.decoded.project);
|
||||
let context = app.route_context().unwrap();
|
||||
let point = Position { x: 345., y: 215. };
|
||||
send(&mut app, Event::Click(context, point));
|
||||
assert!(app.architecture.is_none(), "{}", app.status);
|
||||
assert_eq!(app.active_diagram, Some(diagram));
|
||||
let placed = app.project().diagrams[&diagram]
|
||||
.nodes
|
||||
.values()
|
||||
.find(|n| n.element == sa)
|
||||
.unwrap()
|
||||
.clone();
|
||||
assert_eq!(placed.position, point);
|
||||
let mut expected = before.decoded.project.clone();
|
||||
expected
|
||||
.diagrams
|
||||
.get_mut(&diagram)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.insert(placed.id, placed);
|
||||
assert_eq!(app.project(), &expected);
|
||||
let accepted = app.document.bundle().clone();
|
||||
let _ = app.update(Message::Undo);
|
||||
assert_eq!(app.document.bundle(), &before.bundle);
|
||||
assert!(!app.document.can_undo());
|
||||
let _ = app.update(Message::Redo);
|
||||
assert_eq!(app.project(), &expected);
|
||||
assert_eq!(app.document.bundle(), &accepted);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn traceability_default_does_not_change_creation_or_ordinary_picker_guards() {
|
||||
let mut app = setup(Layer::Oa);
|
||||
let diagram = app.active_diagram.unwrap();
|
||||
send(&mut app, Event::Existing(None));
|
||||
assert_eq!(
|
||||
app.architecture.as_ref().unwrap().saved.filter,
|
||||
Filter::Context
|
||||
);
|
||||
send(&mut app, Event::Cancel);
|
||||
let _ = app.update(Message::Projection(ProjectionKind::Traceability));
|
||||
send(&mut app, Event::Start(Kind::Entity, None));
|
||||
assert_eq!(
|
||||
app.architecture.as_ref().unwrap().saved.filter,
|
||||
Filter::Context
|
||||
);
|
||||
send(&mut app, Event::Cancel);
|
||||
assert_eq!(app.active_diagram, Some(diagram));
|
||||
|
||||
let mut project = app.project().clone();
|
||||
let sa = project
|
||||
.add_element(Kind::Function, Layer::Sa, "Other layer", None)
|
||||
.unwrap();
|
||||
let port = Id::new_v4();
|
||||
project.elements.insert(
|
||||
port,
|
||||
oa_core::Element {
|
||||
id: port,
|
||||
alias: "legacy_port".into(),
|
||||
name: "Port".into(),
|
||||
description: String::new(),
|
||||
parent: Some(sa),
|
||||
body: ElementBody::shared_in(
|
||||
Layer::Sa,
|
||||
SharedBody::Port {
|
||||
contract: PortContract::Legacy,
|
||||
legacy: Some(LegacyPortContract {
|
||||
direction: PortDirection::InOut,
|
||||
interface: None,
|
||||
}),
|
||||
},
|
||||
),
|
||||
},
|
||||
);
|
||||
oa_core::validate_candidate(&project).unwrap();
|
||||
for projection in [ProjectionKind::Overview, ProjectionKind::Traceability] {
|
||||
let mut p = project.clone();
|
||||
p.diagrams.get_mut(&diagram).unwrap().projection = projection;
|
||||
let mut trial = App::with_project(p);
|
||||
trial.page = Page::Diagram;
|
||||
trial.active_diagram = Some(diagram);
|
||||
trial.layer = Layer::Oa;
|
||||
let before = trial.document.bundle().clone();
|
||||
send(&mut trial, Event::Existing(None));
|
||||
send(&mut trial, Event::Filter(Filter::AllCompatible));
|
||||
assert_eq!(
|
||||
candidates(&trial).iter().any(|(id, _, _)| *id == sa),
|
||||
projection == ProjectionKind::Traceability
|
||||
);
|
||||
assert!(!candidates(&trial).iter().any(|(id, _, _)| *id == port));
|
||||
send(&mut trial, Event::Pick(port));
|
||||
assert!(!trial.architecture.as_ref().unwrap().saved.placing);
|
||||
send(&mut trial, Event::Cancel);
|
||||
assert_eq!(trial.document.bundle(), &before);
|
||||
}
|
||||
}
|
||||
@@ -230,7 +230,6 @@ fn architecture_canvas_left_click_uses_world_coordinate_once_at_zoom_and_pan() {
|
||||
pan: app.pan,
|
||||
context: app.route_context().unwrap(),
|
||||
selected_bend: None,
|
||||
selected_attachment: None,
|
||||
editable: true,
|
||||
placing: true,
|
||||
connection: None,
|
||||
@@ -307,7 +306,6 @@ fn architecture_f2_rename_preserves_identity_placement_and_atomic_history() {
|
||||
pan: app.pan,
|
||||
context: app.route_context().unwrap(),
|
||||
selected_bend: None,
|
||||
selected_attachment: None,
|
||||
editable: true,
|
||||
placing: false,
|
||||
connection: None,
|
||||
|
||||
@@ -924,8 +924,6 @@ async fn mounted_bend_delete_has_priority_and_semantic_edge_delete_is_confirmed(
|
||||
diagram,
|
||||
route: EdgeRoute {
|
||||
edge,
|
||||
source_attachment: None,
|
||||
target_attachment: None,
|
||||
waypoints: vec![Position { x: 400., y: -100. }],
|
||||
label: None,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
//! Controller acceptance: one indentation gesture is one draft transaction.
|
||||
use super::*;
|
||||
|
||||
fn fixture(source: &str) -> App {
|
||||
let mut app = App::with_project(Project::empty());
|
||||
app.page = Page::Text;
|
||||
app.source_workspace = Workspace::new(
|
||||
&oa_core::source_project::SourceBundle {
|
||||
entry: "project.arcadia".into(),
|
||||
files: std::collections::BTreeMap::from([("project.arcadia".into(), source.into())]),
|
||||
},
|
||||
true,
|
||||
);
|
||||
app.show_source_file(false);
|
||||
app
|
||||
}
|
||||
fn indent(app: &mut App, outdent: bool) {
|
||||
let message = Message::SourceIndent(app.source_symbol_context(), outdent);
|
||||
let _ = app.update(message);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn indentation_updates_draft_snapshot_version_and_single_undo_redo_without_model_change() {
|
||||
let source = "雨\r\n🚀\r\n";
|
||||
let mut app = fixture(source);
|
||||
app.source_editor.perform(text_editor::Action::SelectAll);
|
||||
let version = app.source_version;
|
||||
let revision = app.source_workspace.revision();
|
||||
let model = app.document.revision();
|
||||
indent(&mut app, false);
|
||||
let after = " 雨\r\n 🚀\r\n";
|
||||
assert_eq!(app.source_editor.text(), after);
|
||||
assert_eq!(app.source_snapshot.as_ref(), after);
|
||||
assert_eq!(app.source_workspace.files["project.arcadia"].text, after);
|
||||
assert!(app.source_workspace.has_edits());
|
||||
assert_eq!(app.source_version, version + 1);
|
||||
assert_eq!(app.source_workspace.revision(), revision + 1);
|
||||
assert_eq!(app.document.revision(), model);
|
||||
let _ = app.update(Message::SourceUndo);
|
||||
assert_eq!(app.source_editor.text(), source);
|
||||
assert!(!app.source_workspace.has_edits());
|
||||
let _ = app.update(Message::SourceRedo);
|
||||
assert_eq!(app.source_editor.text(), after);
|
||||
assert_eq!(app.document.revision(), model);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stale_hidden_and_modal_indentation_is_rejected_before_navigation_invalidation() {
|
||||
for rejected in 0..7 {
|
||||
let mut app = fixture("alpha");
|
||||
let mut context = app.source_symbol_context();
|
||||
match rejected {
|
||||
0 => context.path = "elsewhere.arcadia".into(),
|
||||
1 => context.session = Id::new_v4(),
|
||||
2 => app.page = Page::Home,
|
||||
3 => app.busy = true,
|
||||
4 => app.delete_pending = true,
|
||||
5 => app.hosted.visible = true,
|
||||
_ => app.pending = Some(Pending::New),
|
||||
}
|
||||
let interaction = app.source_symbols.interaction;
|
||||
let version = app.source_version;
|
||||
let _ = app.update(Message::SourceIndent(context, false));
|
||||
assert_eq!(app.source_editor.text(), "alpha");
|
||||
assert_eq!(app.source_version, version);
|
||||
assert_eq!(app.source_symbols.interaction, interaction);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_op_outdent_does_not_add_history_or_increment_source_version() {
|
||||
let mut app = fixture("alpha");
|
||||
let version = app.source_version;
|
||||
indent(&mut app, true);
|
||||
assert_eq!(app.source_version, version);
|
||||
assert!(!app.source_workspace.undo(false));
|
||||
}
|
||||
@@ -33,6 +33,9 @@ fn admission_notice(error: query::AdmissionError) -> String {
|
||||
impl App {
|
||||
pub(super) fn source_input_envelope(&self, message: &Message) -> bool {
|
||||
match message {
|
||||
Message::SourceIndent(context, _) => {
|
||||
context == &self.source_symbol_context() && self.source_navigation_available()
|
||||
}
|
||||
Message::SourceSymbols(context, event) => {
|
||||
context.session == self.source_workspace.session()
|
||||
&& context.path == self.source_workspace.active
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
//! Production App tree and its actual keyboard subscription, without a canvas click.
|
||||
use super::*;
|
||||
use crate::behavior::canvas_workspace::tests::{Mounted, key};
|
||||
use iced::{
|
||||
advanced::subscription,
|
||||
futures::{StreamExt, stream, task::noop_waker_ref},
|
||||
keyboard::key::Named,
|
||||
mouse,
|
||||
};
|
||||
use std::task::{Context as PollContext, Poll};
|
||||
|
||||
async fn press(mounted: &mut Mounted, app: &mut App, named: Named) -> Vec<Message> {
|
||||
let input = key(named);
|
||||
let (mut messages, statuses) = mounted.input_with_status(
|
||||
app,
|
||||
std::slice::from_ref(&input),
|
||||
mouse::Cursor::Unavailable,
|
||||
);
|
||||
let interaction = subscription::Event::Interaction {
|
||||
window: iced::window::Id::unique(),
|
||||
event: input,
|
||||
status: statuses[0],
|
||||
};
|
||||
for recipe in subscription::into_recipes(app.subscription()) {
|
||||
let mut output = recipe.stream(Box::pin(stream::iter([interaction.clone()])));
|
||||
let mut cx = PollContext::from_waker(noop_waker_ref());
|
||||
while let Poll::Ready(Some(message)) = output.poll_next_unpin(&mut cx) {
|
||||
messages.push(message);
|
||||
}
|
||||
}
|
||||
for message in messages.iter().cloned() {
|
||||
let task = app.update(message);
|
||||
mounted.perform(app, task).await;
|
||||
}
|
||||
mounted.settle(app).await;
|
||||
messages
|
||||
}
|
||||
|
||||
async fn armed() -> (App, Mounted, Id) {
|
||||
let mut f = fixture();
|
||||
let diagram = create_view(&mut f).diagram;
|
||||
let mut project = f.app.project().clone();
|
||||
let relation = project.relate(RelationKind::StateTransition, "Switch", f.a, f.b);
|
||||
let mut app = App::with_project(project);
|
||||
app.window_size = iced::Size::new(1800., 1100.);
|
||||
send(&mut app, Message::RevealInDiagram(diagram, relation));
|
||||
let mut mounted = Mounted::new(&mut app).await;
|
||||
app.behavior.diagram.fit = false;
|
||||
let context = app.state_context().unwrap();
|
||||
event(
|
||||
&mut app,
|
||||
Event::ContextMenu {
|
||||
context,
|
||||
object: relation,
|
||||
placement: None,
|
||||
anchor: iced::Point::new(450., 400.),
|
||||
bend: None,
|
||||
route_segment: None,
|
||||
},
|
||||
);
|
||||
mounted.settle(&mut app).await;
|
||||
let token = app.model_menu.as_ref().unwrap().token;
|
||||
send(
|
||||
&mut app,
|
||||
Message::Menu(crate::model_menu::Event::Invoke(
|
||||
token,
|
||||
Box::new(Message::StateDiagram(Event::Choose(
|
||||
Tool::AdjustAttachment {
|
||||
relation,
|
||||
end: EdgeEnd::Source,
|
||||
},
|
||||
))),
|
||||
)),
|
||||
);
|
||||
mounted.settle(&mut app).await;
|
||||
assert!(app.model_menu.is_none());
|
||||
assert_eq!(
|
||||
app.behavior.diagram.tool,
|
||||
Tool::AdjustAttachment {
|
||||
relation,
|
||||
end: EdgeEnd::Source
|
||||
}
|
||||
);
|
||||
(app, mounted, relation)
|
||||
}
|
||||
|
||||
fn canvas_focus(mounted: &mut Mounted, app: &App) -> (iced::Rectangle, bool) {
|
||||
let id = iced::advanced::widget::Id::from("diagram-keyboard-owner");
|
||||
let (_, bounds, focused) = mounted
|
||||
.geometry(app)
|
||||
.focused
|
||||
.into_iter()
|
||||
.find(|(candidate, _, _)| candidate.as_ref() == Some(&id))
|
||||
.unwrap();
|
||||
(bounds, focused)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mounted_menu_armed_attachment_escape_without_canvas_click_preserves_document_and_focus() {
|
||||
let (mut app, mut mounted, _) = armed().await;
|
||||
let before = app.document.snapshot();
|
||||
let history = (app.document.can_undo(), app.document.can_redo());
|
||||
let camera = (
|
||||
app.behavior.diagram.zoom,
|
||||
app.behavior.diagram.pan,
|
||||
app.behavior.diagram.fit,
|
||||
);
|
||||
assert!(!canvas_focus(&mut mounted, &app).1);
|
||||
let messages = press(&mut mounted, &mut app, Named::Escape).await;
|
||||
assert!(messages.iter().any(|m| matches!(m, Message::Escape)));
|
||||
assert_eq!(app.behavior.diagram.tool, Tool::Select);
|
||||
assert!(!canvas_focus(&mut mounted, &app).1);
|
||||
press(&mut mounted, &mut app, Named::Delete).await;
|
||||
assert!(
|
||||
!app.delete_pending,
|
||||
"Escape must not grant canvas Delete ownership"
|
||||
);
|
||||
assert!(Arc::ptr_eq(&before, &app.document.snapshot()));
|
||||
assert_eq!(app.document.bundle(), &before.bundle);
|
||||
assert_eq!(app.project(), &before.decoded.project);
|
||||
assert_eq!((app.document.can_undo(), app.document.can_redo()), history);
|
||||
assert_eq!(
|
||||
(
|
||||
app.behavior.diagram.zoom,
|
||||
app.behavior.diagram.pan,
|
||||
app.behavior.diagram.fit
|
||||
),
|
||||
camera
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mounted_attachment_escape_preserves_modal_priority_and_focused_canvas_control() {
|
||||
let (mut app, mut mounted, _) = armed().await;
|
||||
let before = app.document.snapshot();
|
||||
// Existing modal Escape consumes the first key without canceling the tool underneath.
|
||||
send(&mut app, Message::Delete);
|
||||
assert!(app.delete_pending && app.delete_target.is_some());
|
||||
mounted.settle(&mut app).await;
|
||||
press(&mut mounted, &mut app, Named::Escape).await;
|
||||
assert!(!app.delete_pending);
|
||||
assert!(matches!(
|
||||
app.behavior.diagram.tool,
|
||||
Tool::AdjustAttachment { .. }
|
||||
));
|
||||
// A real canvas click grants ownership; inner-canvas Escape remains live.
|
||||
let bounds = canvas_focus(&mut mounted, &app).0;
|
||||
mounted
|
||||
.click(&mut app, iced::Point::new(bounds.x + 10., bounds.y + 10.))
|
||||
.await;
|
||||
assert!(canvas_focus(&mut mounted, &app).1);
|
||||
let messages = press(&mut mounted, &mut app, Named::Escape).await;
|
||||
assert!(
|
||||
messages
|
||||
.iter()
|
||||
.any(|m| matches!(m, Message::StateDiagram(Event::Choose(Tool::Select))))
|
||||
);
|
||||
assert_eq!(app.behavior.diagram.tool, Tool::Select);
|
||||
assert!(Arc::ptr_eq(&before, &app.document.snapshot()));
|
||||
}
|
||||
@@ -1,480 +0,0 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn state_bend_delayed_commit_after_same_view_selection_is_inert_and_live_commit_works() {
|
||||
let (mut app, _, relation, _) = setup();
|
||||
let context = app.state_context().unwrap();
|
||||
let next = app.project().relations[&relation].target();
|
||||
let waypoints = vec![Position { x: 715., y: -425. }];
|
||||
let delayed = Event::SetBends {
|
||||
context,
|
||||
relation,
|
||||
waypoints: waypoints.clone(),
|
||||
};
|
||||
send(&mut app, Message::Select(next));
|
||||
assert_eq!(app.selected, next);
|
||||
assert_eq!(
|
||||
app.state_context(),
|
||||
Some(context),
|
||||
"same-view selection does not retire the context stamp"
|
||||
);
|
||||
let before = app.document.snapshot();
|
||||
let camera = (
|
||||
app.behavior.diagram.zoom,
|
||||
app.behavior.diagram.pan,
|
||||
app.behavior.diagram.fit,
|
||||
);
|
||||
event(&mut app, delayed);
|
||||
assert_eq!(app.selected, next);
|
||||
assert!(Arc::ptr_eq(&before, &app.document.snapshot()));
|
||||
assert_eq!(app.project(), &before.decoded.project);
|
||||
assert_eq!(app.document.bundle(), &before.bundle);
|
||||
assert!(!app.document.can_undo());
|
||||
assert_eq!(
|
||||
(
|
||||
app.behavior.diagram.zoom,
|
||||
app.behavior.diagram.pan,
|
||||
app.behavior.diagram.fit
|
||||
),
|
||||
camera
|
||||
);
|
||||
send(&mut app, Message::Select(relation));
|
||||
let context = app.state_context().unwrap();
|
||||
event(
|
||||
&mut app,
|
||||
Event::SetBends {
|
||||
context,
|
||||
relation,
|
||||
waypoints: waypoints.clone(),
|
||||
},
|
||||
);
|
||||
let mut expected = before.decoded.project.clone();
|
||||
let route = expected
|
||||
.diagrams
|
||||
.get_mut(&context.diagram)
|
||||
.unwrap()
|
||||
.routes
|
||||
.iter_mut()
|
||||
.find(|route| route.edge == relation)
|
||||
.unwrap();
|
||||
route.waypoints = waypoints;
|
||||
assert_eq!(app.project(), &expected);
|
||||
assert_source_model_agree(&app);
|
||||
send(&mut app, Message::Undo);
|
||||
assert_eq!(app.document.bundle(), &before.bundle);
|
||||
assert!(
|
||||
!app.document.can_undo(),
|
||||
"live drag remains a single transaction"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_bend_menu_carries_exact_segment_and_pin_without_changing_delete_action() {
|
||||
let (mut app, _, relation, _) = setup();
|
||||
let context = app.state_context().unwrap();
|
||||
let point = Position { x: 500., y: -400. };
|
||||
event(
|
||||
&mut app,
|
||||
Event::ContextMenu {
|
||||
context,
|
||||
object: relation,
|
||||
placement: None,
|
||||
anchor: iced::Point::ORIGIN,
|
||||
bend: Some(1),
|
||||
route_segment: Some((2, point)),
|
||||
},
|
||||
);
|
||||
let menu = crate::model_menu::popup(&app).unwrap();
|
||||
let mut add = false;
|
||||
let mut remove = false;
|
||||
let mut delete = false;
|
||||
for entry in menu.entries {
|
||||
if let crate::context_menu::Entry::Action {
|
||||
message: Some(Message::Menu(crate::model_menu::Event::Invoke(_, message))),
|
||||
..
|
||||
} = entry
|
||||
{
|
||||
match *message {
|
||||
Message::StateDiagram(Event::AddBend {
|
||||
relation: id,
|
||||
segment: Some((2, at)),
|
||||
..
|
||||
}) => {
|
||||
assert_eq!(id, relation);
|
||||
assert_eq!(at, point);
|
||||
add = true;
|
||||
}
|
||||
Message::StateDiagram(Event::RemoveBend {
|
||||
relation: id,
|
||||
index: 1,
|
||||
..
|
||||
}) => {
|
||||
assert_eq!(id, relation);
|
||||
remove = true;
|
||||
}
|
||||
Message::Delete => delete = true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(add && remove && delete);
|
||||
}
|
||||
|
||||
fn setup() -> (App, Id, Id, Id) {
|
||||
let mut f = fixture();
|
||||
let diagram = create_view(&mut f).diagram;
|
||||
let mut project = f.app.project().clone();
|
||||
let relation = project.relate(RelationKind::StateTransition, "Bend trial", f.leaf, f.b);
|
||||
let second = state_edit::create_view(&project, f.machine).unwrap();
|
||||
project = prepare_command(&project, &second.command).unwrap().project;
|
||||
project
|
||||
.diagrams
|
||||
.get_mut(&diagram)
|
||||
.unwrap()
|
||||
.routes
|
||||
.push(EdgeRoute {
|
||||
edge: relation,
|
||||
waypoints: vec![
|
||||
Position { x: 650., y: -400. },
|
||||
Position { x: 300., y: -400. },
|
||||
Position { x: 650., y: -400. },
|
||||
],
|
||||
label: Some(EdgeLabel {
|
||||
fraction: 0.35,
|
||||
offset: Position { x: 9., y: -23. },
|
||||
}),
|
||||
source_attachment: Some(EdgeAttachment {
|
||||
side: PortAnchorSide::Top,
|
||||
offset: 0.234567,
|
||||
}),
|
||||
target_attachment: Some(EdgeAttachment {
|
||||
side: PortAnchorSide::Bottom,
|
||||
offset: 0.75,
|
||||
}),
|
||||
});
|
||||
let mut app = App::with_project(project);
|
||||
send(&mut app, Message::RevealInDiagram(diagram, relation));
|
||||
(app, diagram, relation, f.a)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_bend_commands_preserve_other_fields_views_and_exact_history() {
|
||||
let (mut app, diagram, relation, _) = setup();
|
||||
let baseline = app.document.snapshot();
|
||||
let points = vec![Position { x: 715., y: 425. }];
|
||||
let mut expected = app.project().clone();
|
||||
expected.diagrams.get_mut(&diagram).unwrap().routes[0].waypoints = points.clone();
|
||||
let context = app.state_context().unwrap();
|
||||
event(
|
||||
&mut app,
|
||||
Event::SetBends {
|
||||
context,
|
||||
relation,
|
||||
waypoints: points.clone(),
|
||||
},
|
||||
);
|
||||
assert_eq!(app.project(), &expected);
|
||||
assert_source_model_agree(&app);
|
||||
let changed = app.document.snapshot();
|
||||
let context = app.state_context().unwrap();
|
||||
event(
|
||||
&mut app,
|
||||
Event::SetBends {
|
||||
context,
|
||||
relation,
|
||||
waypoints: points,
|
||||
},
|
||||
);
|
||||
assert!(
|
||||
Arc::ptr_eq(&changed, &app.document.snapshot()),
|
||||
"equal bends must not add history"
|
||||
);
|
||||
send(&mut app, Message::Undo);
|
||||
assert_eq!(app.project(), &baseline.decoded.project);
|
||||
assert_eq!(app.document.bundle(), &baseline.bundle);
|
||||
assert!(!app.document.can_undo());
|
||||
send(&mut app, Message::Redo);
|
||||
assert_eq!(app.project(), &expected);
|
||||
assert_eq!(app.document.bundle(), &changed.bundle);
|
||||
let context = app.state_context().unwrap();
|
||||
event(&mut app, Event::ClearBends { context, relation });
|
||||
expected.diagrams.get_mut(&diagram).unwrap().routes[0]
|
||||
.waypoints
|
||||
.clear();
|
||||
assert_eq!(app.project(), &expected);
|
||||
let cleared = app.document.snapshot();
|
||||
let context = app.state_context().unwrap();
|
||||
event(&mut app, Event::ClearBends { context, relation });
|
||||
assert!(Arc::ptr_eq(&cleared, &app.document.snapshot()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_bend_self_loop_add_move_remove_clear_preserves_project_and_source_history() {
|
||||
let (app, diagram, relation, _) = setup();
|
||||
let mut project = app.project().clone();
|
||||
let RelationBody::StateTransition { source, target, .. } =
|
||||
&mut project.relations.get_mut(&relation).unwrap().body
|
||||
else {
|
||||
panic!("State transition fixture")
|
||||
};
|
||||
*target = *source;
|
||||
project.diagrams.get_mut(&diagram).unwrap().routes[0]
|
||||
.waypoints
|
||||
.clear();
|
||||
oa_core::validate_candidate(&project).unwrap();
|
||||
let mut app = App::with_project(project);
|
||||
send(&mut app, Message::RevealInDiagram(diagram, relation));
|
||||
let baseline = app.document.snapshot();
|
||||
let geometry = state_edit::bend_route(app.project(), diagram, relation).unwrap();
|
||||
assert!(!geometry.custom_suspended);
|
||||
assert!(geometry.pins.is_empty());
|
||||
assert!(
|
||||
geometry.points.len() >= 4,
|
||||
"actual external self-loop routing"
|
||||
);
|
||||
let segment = 1;
|
||||
let point = Position {
|
||||
x: (geometry.points[segment].x + geometry.points[segment + 1].x) / 2.,
|
||||
y: (geometry.points[segment].y + geometry.points[segment + 1].y) / 2.,
|
||||
};
|
||||
|
||||
// Each operation must alter only the complete Project's waypoint list;
|
||||
// Undo and Redo must restore exact serialized source, not just geometry.
|
||||
fn check(app: &mut App, diagram: Id, mutation: Event, points: Vec<Position>) {
|
||||
let before = app.document.snapshot();
|
||||
let mut expected = before.decoded.project.clone();
|
||||
expected.diagrams.get_mut(&diagram).unwrap().routes[0].waypoints = points;
|
||||
event(app, mutation);
|
||||
assert_eq!(app.project(), &expected);
|
||||
assert_source_model_agree(app);
|
||||
let after = app.document.snapshot();
|
||||
assert_ne!(after.bundle, before.bundle);
|
||||
send(app, Message::Undo);
|
||||
assert_eq!(app.project(), &before.decoded.project);
|
||||
assert_eq!(app.document.bundle(), &before.bundle);
|
||||
send(app, Message::Redo);
|
||||
assert_eq!(app.project(), &expected);
|
||||
assert_eq!(app.document.bundle(), &after.bundle);
|
||||
assert_source_model_agree(app);
|
||||
}
|
||||
|
||||
let context = app.state_context().unwrap();
|
||||
check(
|
||||
&mut app,
|
||||
diagram,
|
||||
Event::AddBend {
|
||||
context,
|
||||
relation,
|
||||
segment: Some((segment, point)),
|
||||
},
|
||||
vec![point],
|
||||
);
|
||||
let moved = Position {
|
||||
x: point.x + 45.,
|
||||
y: point.y - 35.,
|
||||
};
|
||||
let context = app.state_context().unwrap();
|
||||
check(
|
||||
&mut app,
|
||||
diagram,
|
||||
Event::SetBends {
|
||||
context,
|
||||
relation,
|
||||
waypoints: vec![moved],
|
||||
},
|
||||
vec![moved],
|
||||
);
|
||||
let context = app.state_context().unwrap();
|
||||
check(
|
||||
&mut app,
|
||||
diagram,
|
||||
Event::RemoveBend {
|
||||
context,
|
||||
relation,
|
||||
index: 0,
|
||||
},
|
||||
vec![],
|
||||
);
|
||||
assert_eq!(app.project(), &baseline.decoded.project);
|
||||
send(&mut app, Message::Undo);
|
||||
let context = app.state_context().unwrap();
|
||||
check(
|
||||
&mut app,
|
||||
diagram,
|
||||
Event::ClearBends { context, relation },
|
||||
vec![],
|
||||
);
|
||||
assert_eq!(app.project(), &baseline.decoded.project);
|
||||
assert_source_model_agree(&app);
|
||||
// Clear is a new source-preserving edit and may retain empty-list trivia.
|
||||
// Exact original bytes are restored by history, not by reauthoring a value.
|
||||
for _ in 0..3 {
|
||||
assert!(app.document.can_undo());
|
||||
send(&mut app, Message::Undo);
|
||||
}
|
||||
assert_eq!(app.project(), &baseline.decoded.project);
|
||||
assert_eq!(app.document.bundle(), &baseline.bundle);
|
||||
assert!(!app.document.can_undo());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_bend_insertion_uses_captured_traversal_segment_and_removal_only_changes_pins() {
|
||||
let (app, diagram, relation, _) = setup();
|
||||
let context = app.state_context().unwrap();
|
||||
let p = app.project();
|
||||
let geometry = state_edit::bend_route(p, diagram, relation).unwrap();
|
||||
let original = &p.diagrams[&diagram].routes[0];
|
||||
let reverse = geometry
|
||||
.points
|
||||
.windows(2)
|
||||
.position(|pair| pair[0] == original.waypoints[0] && pair[1] == original.waypoints[1])
|
||||
.unwrap();
|
||||
for (segment, expected_index) in [(reverse, 1), (reverse + 1, 2)] {
|
||||
let point = Position { x: 500., y: -400. };
|
||||
let planned = state_edit::plan(
|
||||
p,
|
||||
context,
|
||||
&Event::AddBend {
|
||||
context,
|
||||
relation,
|
||||
segment: Some((segment, point)),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let actual = prepare_command(p, &planned.command).unwrap().project;
|
||||
let mut expected = p.clone();
|
||||
expected.diagrams.get_mut(&diagram).unwrap().routes[0]
|
||||
.waypoints
|
||||
.insert(expected_index, point);
|
||||
assert_eq!(actual, expected);
|
||||
let removed = state_edit::plan(
|
||||
&actual,
|
||||
context,
|
||||
&Event::RemoveBend {
|
||||
context,
|
||||
relation,
|
||||
index: expected_index,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
prepare_command(&actual, &removed.command).unwrap().project,
|
||||
*p
|
||||
);
|
||||
}
|
||||
let default = state_edit::plan(
|
||||
p,
|
||||
context,
|
||||
&Event::AddBend {
|
||||
context,
|
||||
relation,
|
||||
segment: None,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let Command::SetEdgeRoute { route, .. } = default.command else {
|
||||
panic!("route command")
|
||||
};
|
||||
assert_eq!(route.waypoints.len(), original.waypoints.len() + 1);
|
||||
assert_eq!(route.source_attachment, original.source_attachment);
|
||||
assert_eq!(route.target_attachment, original.target_attachment);
|
||||
assert_eq!(route.label, original.label);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_bend_stale_nonfinite_capacity_hidden_and_folded_requests_are_inert() {
|
||||
let (mut app, diagram, relation, composite) = setup();
|
||||
let context = app.state_context().unwrap();
|
||||
let baseline = app.document.snapshot();
|
||||
for mutation in [
|
||||
Event::SetBends {
|
||||
context: Context {
|
||||
epoch: context.epoch + 1,
|
||||
..context
|
||||
},
|
||||
relation,
|
||||
waypoints: vec![],
|
||||
},
|
||||
Event::SetBends {
|
||||
context,
|
||||
relation,
|
||||
waypoints: vec![Position { x: f32::NAN, y: 0. }],
|
||||
},
|
||||
Event::SetBends {
|
||||
context,
|
||||
relation,
|
||||
waypoints: vec![Position { x: 0., y: 0. }; 129],
|
||||
},
|
||||
Event::AddBend {
|
||||
context,
|
||||
relation,
|
||||
segment: Some((usize::MAX, Position { x: 0., y: 0. })),
|
||||
},
|
||||
Event::RemoveBend {
|
||||
context,
|
||||
relation,
|
||||
index: usize::MAX,
|
||||
},
|
||||
] {
|
||||
event(&mut app, mutation);
|
||||
assert!(Arc::ptr_eq(&baseline, &app.document.snapshot()));
|
||||
}
|
||||
let mut p = app.project().clone();
|
||||
p.diagrams.get_mut(&diagram).unwrap().routes[0].waypoints =
|
||||
vec![Position { x: 500., y: -400. }; 128];
|
||||
assert!(
|
||||
state_edit::plan(
|
||||
&p,
|
||||
context,
|
||||
&Event::AddBend {
|
||||
context,
|
||||
relation,
|
||||
segment: Some((1, Position { x: 500., y: -400. }))
|
||||
}
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
let mut hidden = app.project().clone();
|
||||
hidden
|
||||
.diagrams
|
||||
.get_mut(&diagram)
|
||||
.unwrap()
|
||||
.view_options
|
||||
.hidden_edges
|
||||
.push(relation);
|
||||
assert!(state_edit::bend_route(&hidden, diagram, relation).is_none());
|
||||
assert!(state_edit::plan(&hidden, context, &Event::ClearBends { context, relation }).is_err());
|
||||
let owner = node(&app, diagram, composite);
|
||||
let mut folded = app.project().clone();
|
||||
folded
|
||||
.diagrams
|
||||
.get_mut(&diagram)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&owner)
|
||||
.unwrap()
|
||||
.collapsed = true;
|
||||
assert!(state_edit::bend_route(&folded, diagram, relation).is_none());
|
||||
assert!(state_edit::plan(&folded, context, &Event::ClearBends { context, relation }).is_err());
|
||||
let mut internal = app.project().clone();
|
||||
internal.diagrams.get_mut(&diagram).unwrap().routes.clear();
|
||||
if let RelationBody::StateTransition {
|
||||
source,
|
||||
target,
|
||||
transition_kind,
|
||||
..
|
||||
} = &mut internal.relations.get_mut(&relation).unwrap().body
|
||||
{
|
||||
*target = *source;
|
||||
*transition_kind = TransitionKind::Internal;
|
||||
}
|
||||
assert!(state_edit::bend_route(&internal, diagram, relation).is_none());
|
||||
send(
|
||||
&mut app,
|
||||
Message::Field("/name".into(), "Unapplied bend owner name".into()),
|
||||
);
|
||||
let draft = app.draft.as_ref().unwrap().value.clone();
|
||||
event(&mut app, Event::ClearBends { context, relation });
|
||||
assert!(Arc::ptr_eq(&baseline, &app.document.snapshot()));
|
||||
assert_eq!(app.draft.as_ref().unwrap().value, draft);
|
||||
}
|
||||
@@ -157,14 +157,10 @@ impl App {
|
||||
object,
|
||||
placement,
|
||||
anchor,
|
||||
bend,
|
||||
route_segment,
|
||||
} => {
|
||||
let mut request = crate::model_menu::Request::object(object, anchor);
|
||||
request.canvas = Some(context);
|
||||
request.placement = placement;
|
||||
request.bend = bend;
|
||||
request.route_segment = route_segment;
|
||||
return self.handle(Message::Menu(crate::model_menu::Event::Open(request)));
|
||||
}
|
||||
Event::EditTransition(_, id) => {
|
||||
@@ -176,24 +172,6 @@ impl App {
|
||||
)));
|
||||
}
|
||||
Event::Choose(tool) => {
|
||||
if let Tool::AdjustAttachment { relation, end } = tool {
|
||||
if self.behavior.has_pending() || self.draft_dirty() || self.source_dirty() {
|
||||
self.status =
|
||||
"Apply or cancel pending edits before adjusting an attachment.".into();
|
||||
return Task::none();
|
||||
}
|
||||
let Some(context) = self.state_context() else {
|
||||
return Task::none();
|
||||
};
|
||||
if state_edit::attachment_node(self.project(), context.diagram, relation, end)
|
||||
.is_none()
|
||||
{
|
||||
return Task::none();
|
||||
}
|
||||
self.selected = relation;
|
||||
self.selected_node = None;
|
||||
self.refresh_draft();
|
||||
}
|
||||
if !self.apply() {
|
||||
return Task::none();
|
||||
}
|
||||
@@ -232,37 +210,8 @@ impl App {
|
||||
let Some(context) = self.state_context() else {
|
||||
return Task::none();
|
||||
};
|
||||
if let Event::SetBends { relation, .. } = &mutation
|
||||
&& *relation != self.selected
|
||||
{
|
||||
return Task::none();
|
||||
}
|
||||
if let Event::SetAttachment {
|
||||
relation,
|
||||
end,
|
||||
attachment,
|
||||
..
|
||||
} = &mutation
|
||||
{
|
||||
let existing = self.project().diagrams[&context.diagram]
|
||||
.routes
|
||||
.iter()
|
||||
.find(|route| route.edge == *relation)
|
||||
.and_then(|route| route.attachment(*end));
|
||||
if existing == *attachment {
|
||||
return Task::none();
|
||||
}
|
||||
}
|
||||
match state_edit::plan(self.project(), context, &mutation) {
|
||||
Ok(plan) => {
|
||||
if let Command::SetEdgeRoute { diagram, route } = &plan.command
|
||||
&& crate::route_edit::stored_route(
|
||||
&self.project().diagrams[diagram],
|
||||
route.edge,
|
||||
) == *route
|
||||
{
|
||||
return Task::none();
|
||||
}
|
||||
if self.execute(plan.command) {
|
||||
self.behavior.diagram.diagram = Some(plan.diagram);
|
||||
self.behavior.diagram.placement = plan.placement;
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
//! App transaction and draft boundaries for captured state-canvas messages.
|
||||
use super::*;
|
||||
#[path = "state_attachment_escape_tests.rs"]
|
||||
mod attachment_escape;
|
||||
#[path = "state_bend_tests.rs"]
|
||||
mod bends;
|
||||
use crate::behavior::{
|
||||
self,
|
||||
state_edit::{self, Context, Endpoint, Event, Tool},
|
||||
|
||||
@@ -1,277 +0,0 @@
|
||||
use super::*;
|
||||
use crate::{context_menu::Entry, model_menu, views::traceability::Filter};
|
||||
|
||||
fn send(app: &mut App, message: Message) {
|
||||
let _ = app.update(message);
|
||||
}
|
||||
|
||||
fn fixture() -> (App, Id, Id, Id, Id) {
|
||||
let mut p = Project::empty();
|
||||
let oa = p
|
||||
.add_element(Kind::Activity, Layer::Oa, "Observe", None)
|
||||
.unwrap();
|
||||
let sa = p
|
||||
.add_element(Kind::Function, Layer::Sa, "Detect", None)
|
||||
.unwrap();
|
||||
let mixed = p.ensure_diagram(Layer::Oa);
|
||||
p.diagrams.get_mut(&mixed).unwrap().projection = ProjectionKind::Traceability;
|
||||
p.place(mixed, oa, Position { x: 300., y: 200. });
|
||||
let system = p.ensure_diagram(Layer::Sa);
|
||||
p.place(system, sa, Position { x: 700., y: 200. });
|
||||
oa_core::validate_candidate(&p).unwrap();
|
||||
let mut app = App::with_project(p);
|
||||
app.page = Page::Diagram;
|
||||
app.layer = Layer::Oa;
|
||||
app.active_diagram = Some(mixed);
|
||||
app.selected = oa;
|
||||
app.refresh_draft();
|
||||
app.traceability.filter = Filter::Artifacts;
|
||||
app.traceability.query = "stale artifact query".into();
|
||||
(app, mixed, system, oa, sa)
|
||||
}
|
||||
|
||||
fn show_action(app: &mut App, element: Id) -> Message {
|
||||
send(app, model_menu::open(element, iced::Point::ORIGIN));
|
||||
model_menu::popup(app)
|
||||
.unwrap()
|
||||
.entries
|
||||
.into_iter()
|
||||
.find_map(|entry| match entry {
|
||||
Entry::Action { label, message, .. } if label == "Show in this diagram" => {
|
||||
Some(message)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.expect("eligible real menu action")
|
||||
.expect("eligible menu action must be enabled")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn review_layer_traces_resets_inventory_without_applying_property_or_source_drafts() {
|
||||
for source_draft in [false, true] {
|
||||
let (mut app, mixed, _, oa, _) = fixture();
|
||||
if source_draft {
|
||||
send(&mut app, Message::Page(Page::Text));
|
||||
send(
|
||||
&mut app,
|
||||
Message::SourceEdit(iced::widget::text_editor::Action::Edit(
|
||||
iced::widget::text_editor::Edit::Insert('x'),
|
||||
)),
|
||||
);
|
||||
assert!(app.source_dirty());
|
||||
} else {
|
||||
send(
|
||||
&mut app,
|
||||
Message::Field("/alias".into(), "invalid alias".into()),
|
||||
);
|
||||
assert!(app.draft_dirty());
|
||||
}
|
||||
let raw = serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap();
|
||||
let source = app.source_editor.text();
|
||||
let before = app.document.snapshot();
|
||||
let camera = (app.zoom, app.pan);
|
||||
send(&mut app, Message::ReviewLayerTraces);
|
||||
assert_eq!(app.page, Page::Traces);
|
||||
assert_eq!(app.traceability.filter, Filter::Traces);
|
||||
assert!(app.traceability.query.is_empty());
|
||||
assert_eq!(
|
||||
(app.layer, app.active_diagram, app.selected),
|
||||
(Layer::Oa, Some(mixed), oa)
|
||||
);
|
||||
assert_eq!((app.zoom, app.pan), camera);
|
||||
assert_eq!(
|
||||
serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(),
|
||||
raw
|
||||
);
|
||||
assert_eq!(app.source_editor.text(), source);
|
||||
assert!(Arc::ptr_eq(&before, &app.document.snapshot()));
|
||||
assert!(!app.document.can_undo());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn review_layer_traces_respects_busy_modal_and_active_placement_guards() {
|
||||
for already_traces in [false, true] {
|
||||
for guard in 0..5 {
|
||||
let (mut app, mixed, _, oa, _) = fixture();
|
||||
match guard {
|
||||
0 => app.busy = true,
|
||||
1 => app.delete_pending = true,
|
||||
2 => app.hosted.visible = true,
|
||||
3 => send(
|
||||
&mut app,
|
||||
Message::Architecture(architecture_authoring::Event::Existing(None)),
|
||||
),
|
||||
4 => {
|
||||
app.page = Page::Behavior;
|
||||
let p = app.project().clone();
|
||||
app.behavior
|
||||
.creation
|
||||
.update(&p, behavior::creation::Event::Open(Layer::Oa))
|
||||
.unwrap();
|
||||
assert!(app.behavior.has_pending());
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
if already_traces {
|
||||
app.page = Page::Traces;
|
||||
}
|
||||
let original_page = app.page;
|
||||
let before = app.document.snapshot();
|
||||
let behavior_before = app.behavior.recovery_snapshot().unwrap();
|
||||
send(&mut app, Message::ReviewLayerTraces);
|
||||
assert_eq!(
|
||||
app.page, original_page,
|
||||
"guard {guard}, already traces {already_traces}"
|
||||
);
|
||||
assert_eq!(app.traceability.filter, Filter::Artifacts);
|
||||
assert_eq!(app.traceability.query, "stale artifact query");
|
||||
assert_eq!(
|
||||
(app.layer, app.active_diagram, app.selected),
|
||||
(Layer::Oa, Some(mixed), oa)
|
||||
);
|
||||
assert!(Arc::ptr_eq(&before, &app.document.snapshot()));
|
||||
assert_eq!(app.behavior.recovery_snapshot().unwrap(), behavior_before);
|
||||
if guard == 3 {
|
||||
assert!(app.architecture.is_some());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixed_view_show_keeps_its_context_for_every_diagram_app_and_target_layer() {
|
||||
for diagram_layer in [Layer::Oa, Layer::Sa] {
|
||||
for app_layer in [Layer::Oa, Layer::Sa] {
|
||||
for target_layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut p = Project::empty();
|
||||
let oa = p
|
||||
.add_element(Kind::Activity, Layer::Oa, "Observe", None)
|
||||
.unwrap();
|
||||
let sa = p
|
||||
.add_element(Kind::Function, Layer::Sa, "Detect", None)
|
||||
.unwrap();
|
||||
let target = if target_layer == Layer::Oa { oa } else { sa };
|
||||
let other = if target_layer == Layer::Oa { sa } else { oa };
|
||||
let mixed = p.ensure_diagram(diagram_layer);
|
||||
p.diagrams.get_mut(&mixed).unwrap().projection = ProjectionKind::Traceability;
|
||||
// An attractive same-layer alternative makes accidental
|
||||
// semantic-selection navigation observable in both directions.
|
||||
let mut alternative = p.diagrams[&mixed].clone();
|
||||
alternative.id = Id::new_v4();
|
||||
alternative.alias = "same_layer_alternative".into();
|
||||
alternative.layer = target_layer;
|
||||
alternative.projection = ProjectionKind::Overview;
|
||||
let alternative_id = alternative.id;
|
||||
p.diagrams.insert(alternative_id, alternative);
|
||||
p.place(alternative_id, target, Position { x: 700., y: 200. });
|
||||
oa_core::validate_candidate(&p).unwrap();
|
||||
let mut app = App::with_project(p);
|
||||
app.page = Page::Diagram;
|
||||
app.layer = app_layer;
|
||||
app.active_diagram = Some(mixed);
|
||||
app.selected = other;
|
||||
app.refresh_draft();
|
||||
let before = app.project().clone();
|
||||
let action = show_action(&mut app, target);
|
||||
send(&mut app, action);
|
||||
// The existing RevealInDiagram action synchronizes the layer
|
||||
// to the captured view, even if the prior perspective differed.
|
||||
assert_eq!(
|
||||
(app.active_diagram, app.layer, app.selected),
|
||||
(Some(mixed), diagram_layer, target)
|
||||
);
|
||||
let added = app.project().diagrams[&mixed]
|
||||
.nodes
|
||||
.values()
|
||||
.find(|n| n.element == target)
|
||||
.unwrap()
|
||||
.clone();
|
||||
assert_eq!(app.selected_node, Some(added.id));
|
||||
let mut expected = before;
|
||||
expected
|
||||
.diagrams
|
||||
.get_mut(&mixed)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.insert(added.id, added);
|
||||
assert_eq!(app.project(), &expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixed_view_show_menu_places_in_captured_view_with_exact_undo_redo() {
|
||||
let (mut app, mixed, system, _, sa) = fixture();
|
||||
let before = app.document.snapshot();
|
||||
let action = show_action(&mut app, sa);
|
||||
send(&mut app, action);
|
||||
assert_eq!(
|
||||
(app.active_diagram, app.layer, app.selected),
|
||||
(Some(mixed), Layer::Oa, sa)
|
||||
);
|
||||
let added = app.project().diagrams[&mixed]
|
||||
.nodes
|
||||
.values()
|
||||
.find(|n| n.element == sa)
|
||||
.unwrap()
|
||||
.clone();
|
||||
assert_eq!(added.position, Position { x: 60., y: 60. });
|
||||
let mut expected = before.decoded.project.clone();
|
||||
expected
|
||||
.diagrams
|
||||
.get_mut(&mixed)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.insert(added.id, added);
|
||||
assert_eq!(app.project(), &expected);
|
||||
assert_eq!(
|
||||
app.project().diagrams[&system],
|
||||
before.decoded.project.diagrams[&system]
|
||||
);
|
||||
let after = app.document.snapshot();
|
||||
send(&mut app, Message::Undo);
|
||||
assert_eq!(app.project(), &before.decoded.project);
|
||||
assert_eq!(app.document.bundle(), &before.bundle);
|
||||
assert!(!app.document.can_undo());
|
||||
send(&mut app, Message::Redo);
|
||||
assert_eq!(app.project(), &after.decoded.project);
|
||||
assert_eq!(app.document.bundle(), &after.bundle);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixed_view_show_menu_rejects_stale_context_and_pending_property_changes() {
|
||||
for refusal in 0..3 {
|
||||
let (mut app, mixed, system, oa, sa) = fixture();
|
||||
let action = show_action(&mut app, sa);
|
||||
match refusal {
|
||||
0 => app.active_diagram = Some(system),
|
||||
1 => app.canvas_epoch = app.canvas_epoch.wrapping_add(1),
|
||||
2 => {
|
||||
// A queued action must not apply a draft that became dirty
|
||||
// after the menu was rendered, even when its context is valid.
|
||||
app.draft
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.input("/name", "Unapplied name".into());
|
||||
assert!(app.draft_dirty());
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
let before = app.document.snapshot();
|
||||
let raw = serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap();
|
||||
send(&mut app, action);
|
||||
assert!(Arc::ptr_eq(&before, &app.document.snapshot()));
|
||||
assert_eq!(app.selected, oa);
|
||||
assert_eq!(
|
||||
app.active_diagram,
|
||||
Some(if refusal == 0 { system } else { mixed })
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(),
|
||||
raw
|
||||
);
|
||||
assert!(!app.document.can_undo());
|
||||
}
|
||||
}
|
||||
@@ -158,14 +158,6 @@ impl Mounted {
|
||||
events: &[iced::Event],
|
||||
cursor: mouse::Cursor,
|
||||
) -> Vec<Message> {
|
||||
self.input_with_status(app, events, cursor).0
|
||||
}
|
||||
pub fn input_with_status(
|
||||
&mut self,
|
||||
app: &App,
|
||||
events: &[iced::Event],
|
||||
cursor: mouse::Cursor,
|
||||
) -> (Vec<Message>, Vec<iced::event::Status>) {
|
||||
let mut messages = vec![];
|
||||
let mut ui = UserInterface::build(
|
||||
app.view(),
|
||||
@@ -173,7 +165,7 @@ impl Mounted {
|
||||
std::mem::take(&mut self.cache),
|
||||
&mut self.renderer,
|
||||
);
|
||||
let (_, statuses) = ui.update(
|
||||
let _ = ui.update(
|
||||
events,
|
||||
cursor,
|
||||
&mut self.renderer,
|
||||
@@ -181,7 +173,7 @@ impl Mounted {
|
||||
&mut messages,
|
||||
);
|
||||
self.cache = ui.into_cache();
|
||||
(messages, statuses)
|
||||
messages
|
||||
}
|
||||
pub async fn perform(&mut self, app: &mut App, task: iced::Task<Message>) {
|
||||
let mut ready = vec![];
|
||||
|
||||
@@ -1,656 +0,0 @@
|
||||
// Included in state_canvas::tests: exercise the real canvas event path.
|
||||
|
||||
fn bend_fixture(authored: bool) -> (Fixture, Id) {
|
||||
let mut f = fixture();
|
||||
let relation = f.project.relate(
|
||||
RelationKind::StateTransition,
|
||||
"Standby to stopped",
|
||||
f.b,
|
||||
f.c,
|
||||
);
|
||||
if authored {
|
||||
f.project
|
||||
.diagrams
|
||||
.get_mut(&f.diagram)
|
||||
.unwrap()
|
||||
.routes
|
||||
.push(EdgeRoute {
|
||||
edge: relation,
|
||||
waypoints: vec![Position {
|
||||
x: 650.25,
|
||||
y: 340.75,
|
||||
}],
|
||||
source_attachment: Some(EdgeAttachment {
|
||||
side: PortAnchorSide::Bottom,
|
||||
offset: 0.5,
|
||||
}),
|
||||
target_attachment: Some(EdgeAttachment {
|
||||
side: PortAnchorSide::Top,
|
||||
offset: 0.25,
|
||||
}),
|
||||
label: Some(EdgeLabel {
|
||||
fraction: 0.43,
|
||||
offset: Position { x: 28., y: -16. },
|
||||
}),
|
||||
});
|
||||
}
|
||||
validate_candidate(&f.project).unwrap();
|
||||
(f, relation)
|
||||
}
|
||||
|
||||
fn bend_segment(scene: &Scene, relation: Id) -> (usize, Point) {
|
||||
let edge = &scene.routed.edges[&relation];
|
||||
edge.points
|
||||
.windows(2)
|
||||
.enumerate()
|
||||
.filter(|(i, _)| *i > 0 && *i + 2 < edge.points.len())
|
||||
.map(|(i, p)| {
|
||||
(
|
||||
i,
|
||||
Point::new((p[0].x + p[1].x) / 2., (p[0].y + p[1].y) / 2.),
|
||||
(p[1].x - p[0].x).abs() + (p[1].y - p[0].y).abs(),
|
||||
)
|
||||
})
|
||||
.filter(|(_, p, length)| {
|
||||
*length > 40. && edge.pins.iter().all(|pin| p.distance(point(*pin)) > 20.)
|
||||
})
|
||||
.max_by(|a, b| a.2.total_cmp(&b.2))
|
||||
.map(|(i, p, _)| (i, p))
|
||||
.expect("fixture has an exposed interior route segment")
|
||||
}
|
||||
|
||||
fn bend_press(canvas: &StateCanvas<'_>, state: &mut State, at: Point) {
|
||||
motion(canvas, state, at, at);
|
||||
let event = input(
|
||||
canvas,
|
||||
state,
|
||||
mouse::Event::ButtonPressed(mouse::Button::Left),
|
||||
at,
|
||||
);
|
||||
assert!(
|
||||
!matches!(event, Some(Event::SetBends { .. })),
|
||||
"press alone cannot commit"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_bends_segment_drag_inserts_one_pin_using_ordered_pointer_at_three_zooms() {
|
||||
for zoom in [0.5, 1., 2.] {
|
||||
let (f, relation) = bend_fixture(false);
|
||||
let mut canvas = canvas(&f, Tool::Select, zoom);
|
||||
canvas.selected = relation;
|
||||
let mut state = State::default();
|
||||
let baseline = canvas.scene(&state).unwrap();
|
||||
let (_, midpoint) = bend_segment(&baseline, relation);
|
||||
let start = screen(&canvas, midpoint);
|
||||
let destination = Position {
|
||||
x: ((midpoint.x + 45.) / 5.).round() * 5.,
|
||||
y: ((midpoint.y + 30.) / 5.).round() * 5.,
|
||||
};
|
||||
let release = screen(&canvas, point(destination));
|
||||
let later_batch_cursor = release + Vector::new(137., 83.);
|
||||
motion(&canvas, &mut state, start, later_batch_cursor);
|
||||
input(
|
||||
&canvas,
|
||||
&mut state,
|
||||
mouse::Event::ButtonPressed(mouse::Button::Left),
|
||||
later_batch_cursor,
|
||||
);
|
||||
assert!(f.project.diagrams[&f.diagram].routes.is_empty());
|
||||
motion(&canvas, &mut state, release, later_batch_cursor);
|
||||
assert_eq!(
|
||||
state.bend_preview.as_ref().unwrap().waypoints,
|
||||
vec![destination]
|
||||
);
|
||||
assert_eq!(
|
||||
canvas.scene(&state).unwrap().routed.edges[&relation].pins,
|
||||
vec![destination]
|
||||
);
|
||||
let event = input(
|
||||
&canvas,
|
||||
&mut state,
|
||||
mouse::Event::ButtonReleased(mouse::Button::Left),
|
||||
later_batch_cursor,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
matches!(&event, Event::SetBends { context, relation: id, waypoints }
|
||||
if *context == canvas.context && *id == relation && *waypoints == vec![destination])
|
||||
);
|
||||
assert!(state.bend_preview.is_none());
|
||||
assert!(
|
||||
input(
|
||||
&canvas,
|
||||
&mut state,
|
||||
mouse::Event::ButtonReleased(mouse::Button::Left),
|
||||
later_batch_cursor
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
let after = prepare_command(
|
||||
&f.project,
|
||||
&state_edit::plan(&f.project, canvas.context, &event)
|
||||
.unwrap()
|
||||
.command,
|
||||
)
|
||||
.unwrap()
|
||||
.project;
|
||||
let stored = &after.diagrams[&f.diagram].routes[0];
|
||||
assert_eq!(stored.waypoints, vec![destination]);
|
||||
assert!(
|
||||
stored.source_attachment.is_none()
|
||||
&& stored.target_attachment.is_none()
|
||||
&& stored.label.is_none()
|
||||
);
|
||||
assert_eq!(after.relations, f.project.relations);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_bends_authored_pin_move_preserves_full_project_and_exact_undo_redo() {
|
||||
for zoom in [0.5, 1., 2.] {
|
||||
let (mut f, relation) = bend_fixture(true);
|
||||
let mut other = f.project.diagrams[&f.diagram].clone();
|
||||
other.id = Id::new_v4();
|
||||
other.alias = "bend_reference_view".into();
|
||||
other.nodes = other
|
||||
.nodes
|
||||
.into_values()
|
||||
.map(|mut node| {
|
||||
node.id = Id::new_v4();
|
||||
node.alias = format!("bend_reference_{}", node.alias);
|
||||
(node.id, node)
|
||||
})
|
||||
.collect();
|
||||
let other_id = other.id;
|
||||
f.project.diagrams.insert(other_id, other.clone());
|
||||
validate_candidate(&f.project).unwrap();
|
||||
let original = f.project.diagrams[&f.diagram].routes[0].clone();
|
||||
let mut canvas = canvas(&f, Tool::Select, zoom);
|
||||
canvas.selected = relation;
|
||||
let mut state = State::default();
|
||||
let start = screen(&canvas, point(original.waypoints[0]));
|
||||
let destination = Position { x: 690., y: 365. };
|
||||
let release = screen(&canvas, point(destination));
|
||||
bend_press(&canvas, &mut state, start);
|
||||
motion(&canvas, &mut state, release, release);
|
||||
let preview = state.bend_preview.as_ref().unwrap();
|
||||
let mut intended = original.clone();
|
||||
intended.waypoints[0] = destination;
|
||||
assert_eq!(preview, &intended);
|
||||
assert_eq!(
|
||||
f.project.diagrams[&f.diagram].routes[0], original,
|
||||
"preview is transient"
|
||||
);
|
||||
let preview_scene = canvas.scene(&state).unwrap();
|
||||
let event = input(
|
||||
&canvas,
|
||||
&mut state,
|
||||
mouse::Event::ButtonReleased(mouse::Button::Left),
|
||||
release,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
matches!(&event, Event::SetBends { waypoints, .. } if waypoints == &intended.waypoints)
|
||||
);
|
||||
let plan = state_edit::plan(&f.project, canvas.context, &event).unwrap();
|
||||
let mut expected = f.project.clone();
|
||||
expected.diagrams.get_mut(&f.diagram).unwrap().routes[0] = intended;
|
||||
let mut document = Document::from_project(f.project.clone(), Default::default()).unwrap();
|
||||
let before = document.bundle().clone();
|
||||
document.execute(plan.command).unwrap();
|
||||
assert_eq!(document.project(), &expected);
|
||||
assert_eq!(document.project().diagrams[&other_id], other);
|
||||
let committed_view = projection::project_view(document.project(), f.diagram);
|
||||
let committed = routing::route_diagram(
|
||||
document.project(),
|
||||
&document.project().diagrams[&f.diagram],
|
||||
&committed_view,
|
||||
);
|
||||
let preview_edge = &preview_scene.routed.edges[&relation];
|
||||
let committed_edge = &committed.edges[&relation];
|
||||
assert_eq!(preview_edge.points, committed_edge.points);
|
||||
assert_eq!(preview_edge.pin_fractions, committed_edge.pin_fractions);
|
||||
assert_eq!(preview_edge.warnings, committed_edge.warnings);
|
||||
let edited = document.bundle().clone();
|
||||
assert!(document.undo());
|
||||
assert_eq!(document.project(), &f.project);
|
||||
assert_eq!(document.bundle(), &before);
|
||||
assert!(!document.undo(), "one drag adds one history operation");
|
||||
assert!(document.redo());
|
||||
assert_eq!(document.project(), &expected);
|
||||
assert_eq!(document.bundle(), &edited);
|
||||
assert!(!document.redo());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_bends_stationary_and_out_and_back_restore_off_grid_or_automatic_route() {
|
||||
for authored in [false, true] {
|
||||
for zoom in [0.5, 1., 2.] {
|
||||
for travel in [false, true] {
|
||||
let (f, relation) = bend_fixture(authored);
|
||||
let mut canvas = canvas(&f, Tool::Select, zoom);
|
||||
canvas.selected = relation;
|
||||
let mut state = State::default();
|
||||
let scene = canvas.scene(&state).unwrap();
|
||||
let world = if authored {
|
||||
point(scene.routed.edges[&relation].pins[0])
|
||||
} else {
|
||||
bend_segment(&scene, relation).1
|
||||
};
|
||||
let start = screen(&canvas, world);
|
||||
bend_press(&canvas, &mut state, start);
|
||||
if travel {
|
||||
let away = start + Vector::new(45., 30.) * zoom;
|
||||
motion(&canvas, &mut state, away, away);
|
||||
assert!(state.bend_preview.is_some());
|
||||
motion(&canvas, &mut state, start, start);
|
||||
}
|
||||
assert!(
|
||||
input(
|
||||
&canvas,
|
||||
&mut state,
|
||||
mouse::Event::ButtonReleased(mouse::Button::Left),
|
||||
start
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
assert!(state.bend_preview.is_none());
|
||||
assert_eq!(
|
||||
canvas.scene(&state).unwrap().routed.edges[&relation].points,
|
||||
scene.routed.edges[&relation].points
|
||||
);
|
||||
assert_eq!(
|
||||
f.project.diagrams[&f.diagram].routes.len(),
|
||||
usize::from(authored)
|
||||
);
|
||||
if authored {
|
||||
assert_eq!(
|
||||
f.project.diagrams[&f.diagram].routes[0].waypoints,
|
||||
vec![Position {
|
||||
x: 650.25,
|
||||
y: 340.75
|
||||
}]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_bends_cancel_on_camera_context_focus_and_pointer_interruptions() {
|
||||
for cancellation in 0..16 {
|
||||
let (f, relation) = bend_fixture(true);
|
||||
let mut canvas = canvas(&f, Tool::Select, 1.);
|
||||
canvas.selected = relation;
|
||||
let mut state = State::default();
|
||||
let original = f.project.diagrams[&f.diagram].routes[0].clone();
|
||||
let start = screen(&canvas, point(original.waypoints[0]));
|
||||
let release = start + Vector::new(45., 30.);
|
||||
bend_press(&canvas, &mut state, start);
|
||||
motion(&canvas, &mut state, release, release);
|
||||
assert!(state.bend_preview.is_some(), "case {cancellation}");
|
||||
let mut release_bounds = bounds();
|
||||
match cancellation {
|
||||
0 => canvas.zoom = 2.,
|
||||
1 => canvas.pan.x += 10.,
|
||||
2 => canvas.context.epoch += 1,
|
||||
3 => canvas.context.revision += 1,
|
||||
4 => canvas.context.document = Id::new_v4(),
|
||||
5 => canvas.tool = Tool::Transition,
|
||||
6 => canvas.editable = false,
|
||||
7 | 8 => {
|
||||
input(
|
||||
&canvas,
|
||||
&mut state,
|
||||
mouse::Event::ButtonPressed(if cancellation == 7 {
|
||||
mouse::Button::Right
|
||||
} else {
|
||||
mouse::Button::Middle
|
||||
}),
|
||||
release,
|
||||
);
|
||||
}
|
||||
9 => {
|
||||
canvas::Program::update(
|
||||
&canvas,
|
||||
&mut state,
|
||||
&canvas::Event::Window(iced::window::Event::Unfocused),
|
||||
bounds(),
|
||||
mouse::Cursor::Available(release),
|
||||
);
|
||||
}
|
||||
10 => {
|
||||
input(
|
||||
&canvas,
|
||||
&mut state,
|
||||
mouse::Event::WheelScrolled {
|
||||
delta: mouse::ScrollDelta::Lines { x: 0., y: 1. },
|
||||
},
|
||||
release,
|
||||
);
|
||||
}
|
||||
11 => {
|
||||
input(&canvas, &mut state, mouse::Event::CursorLeft, release);
|
||||
}
|
||||
12 => {
|
||||
motion(&canvas, &mut state, Point::new(-100., -100.), release);
|
||||
}
|
||||
13 => release_bounds.x += 25.,
|
||||
15 => canvas.selected = f.b,
|
||||
_ => {
|
||||
let escape = canvas::Event::Keyboard(keyboard::Event::KeyPressed {
|
||||
key: keyboard::Key::Named(keyboard::key::Named::Escape),
|
||||
modified_key: keyboard::Key::Named(keyboard::key::Named::Escape),
|
||||
physical_key: keyboard::key::Physical::Code(keyboard::key::Code::Escape),
|
||||
location: keyboard::Location::Standard,
|
||||
modifiers: keyboard::Modifiers::empty(),
|
||||
text: None,
|
||||
repeat: false,
|
||||
});
|
||||
let event = canvas::Program::update(
|
||||
&canvas,
|
||||
&mut state,
|
||||
&escape,
|
||||
bounds(),
|
||||
mouse::Cursor::Available(release),
|
||||
)
|
||||
.and_then(|action| action.into_inner().0);
|
||||
assert!(matches!(event, Some(Event::Choose(Tool::Select))));
|
||||
}
|
||||
}
|
||||
let event = input_at_bounds(
|
||||
&canvas,
|
||||
&mut state,
|
||||
mouse::Event::ButtonReleased(mouse::Button::Left),
|
||||
release,
|
||||
release_bounds,
|
||||
);
|
||||
assert!(
|
||||
!matches!(event, Some(Event::SetBends { .. })),
|
||||
"late release committed case {cancellation}"
|
||||
);
|
||||
assert!(
|
||||
state.bend_preview.is_none(),
|
||||
"stale preview survived case {cancellation}"
|
||||
);
|
||||
assert_eq!(f.project.diagrams[&f.diagram].routes[0], original);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_bends_context_menu_distinguishes_authored_pin_from_computed_segment() {
|
||||
let (f, relation) = bend_fixture(true);
|
||||
let mut canvas = canvas(&f, Tool::Select, 1.);
|
||||
canvas.selected = relation;
|
||||
let mut state = State::default();
|
||||
let scene = canvas.scene(&state).unwrap();
|
||||
let pin = screen(&canvas, point(scene.routed.edges[&relation].pins[0]));
|
||||
motion(&canvas, &mut state, pin, pin);
|
||||
assert!(
|
||||
matches!(input(&canvas, &mut state, mouse::Event::ButtonPressed(mouse::Button::Right), pin),
|
||||
Some(Event::ContextMenu { context, object, placement: None, anchor, bend: Some(0), route_segment: None })
|
||||
if context == canvas.context && object == relation && anchor == pin)
|
||||
);
|
||||
let (index, midpoint) = bend_segment(&scene, relation);
|
||||
let at = screen(&canvas, midpoint);
|
||||
motion(&canvas, &mut state, at, at);
|
||||
assert!(
|
||||
matches!(input(&canvas, &mut state, mouse::Event::ButtonPressed(mouse::Button::Right), at),
|
||||
Some(Event::ContextMenu { context, object, placement: None, anchor, bend: None, route_segment: Some((segment, world)) })
|
||||
if context == canvas.context && object == relation && anchor == at && segment == index && point(world).distance(midpoint) < 0.001)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_bends_internal_rows_and_folded_proxies_cannot_acquire_pins() {
|
||||
for internal in [false, true] {
|
||||
let mut f = fixture();
|
||||
let relation = if internal {
|
||||
let id = f
|
||||
.project
|
||||
.relate(RelationKind::StateTransition, "Internal check", f.a, f.a);
|
||||
if let RelationBody::StateTransition {
|
||||
transition_kind, ..
|
||||
} = &mut f.project.relations.get_mut(&id).unwrap().body
|
||||
{
|
||||
*transition_kind = TransitionKind::Internal;
|
||||
}
|
||||
id
|
||||
} else {
|
||||
let id = f.project.relate(
|
||||
RelationKind::StateTransition,
|
||||
"Hidden child to standby",
|
||||
f.leaf,
|
||||
f.b,
|
||||
);
|
||||
let parent = placement(&f, f.a);
|
||||
f.project
|
||||
.diagrams
|
||||
.get_mut(&f.diagram)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&parent)
|
||||
.unwrap()
|
||||
.collapsed = true;
|
||||
id
|
||||
};
|
||||
let mut canvas = canvas(&f, Tool::Select, 1.);
|
||||
canvas.selected = relation;
|
||||
let mut state = State::default();
|
||||
let scene = canvas.scene(&state).unwrap();
|
||||
let world = if internal {
|
||||
assert!(!scene.routed.edges.contains_key(&relation));
|
||||
let node = scene.view.nodes.iter().find(|n| n.element == f.a).unwrap();
|
||||
internal_rows(&f.project, node)
|
||||
.into_iter()
|
||||
.find(|(r, _)| r.id == relation)
|
||||
.unwrap()
|
||||
.1
|
||||
.center()
|
||||
} else {
|
||||
assert!(scene.routed.edges[&relation].custom_suspended);
|
||||
bend_segment(&scene, relation).1
|
||||
};
|
||||
let start = screen(&canvas, world);
|
||||
motion(&canvas, &mut state, start, start);
|
||||
assert!(matches!(
|
||||
input(
|
||||
&canvas,
|
||||
&mut state,
|
||||
mouse::Event::ButtonPressed(mouse::Button::Right),
|
||||
start
|
||||
),
|
||||
Some(Event::ContextMenu {
|
||||
bend: None,
|
||||
route_segment: None,
|
||||
..
|
||||
})
|
||||
));
|
||||
bend_press(&canvas, &mut state, start);
|
||||
let away = start + Vector::new(30., 20.);
|
||||
motion(&canvas, &mut state, away, away);
|
||||
assert!(state.bend_preview.is_none());
|
||||
assert!(!matches!(
|
||||
input(
|
||||
&canvas,
|
||||
&mut state,
|
||||
mouse::Event::ButtonReleased(mouse::Button::Left),
|
||||
away
|
||||
),
|
||||
Some(Event::SetBends { .. })
|
||||
));
|
||||
assert!(f.project.diagrams[&f.diagram].routes.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_bends_right_click_discards_provisional_indexes_before_building_menu() {
|
||||
for click_pin in [true, false] {
|
||||
let (mut f, relation) = bend_fixture(true);
|
||||
let route = &mut f.project.diagrams.get_mut(&f.diagram).unwrap().routes[0];
|
||||
route.waypoints = vec![Position { x: 650., y: 340. }, Position { x: 650., y: 390. }];
|
||||
validate_candidate(&f.project).unwrap();
|
||||
let original = f.project.diagrams[&f.diagram].routes[0].clone();
|
||||
let mut canvas = canvas(&f, Tool::Select, 1.);
|
||||
canvas.selected = relation;
|
||||
let mut state = State::default();
|
||||
let baseline = canvas.scene(&state).unwrap();
|
||||
let edge = &baseline.routed.edges[&relation];
|
||||
let candidates: Vec<_> = edge.points.windows(2).enumerate()
|
||||
.filter(|(i, _)| *i > 0 && *i + 2 < edge.points.len())
|
||||
.map(|(i, p)| (i, Point::new((p[0].x + p[1].x) / 2., (p[0].y + p[1].y) / 2.)))
|
||||
.filter(|(_, p)| edge.pins.iter().all(|pin| p.distance(point(*pin)) > 20.))
|
||||
.filter(|(_, p)| matches!(canvas.hit(&baseline, *p), Some(Hit::Transition(id)) if id == relation))
|
||||
.filter(|(_, p)| !Rectangle::new(point(edge.label.bounds.position), Size::new(edge.label.bounds.size.width, edge.label.bounds.size.height)).contains(*p))
|
||||
.collect();
|
||||
let (_, insertion_at) = candidates
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|(i, p)| routing::pin_insertion_index(edge, *i, position(*p)) == 0)
|
||||
.expect("exposed segment before both authored pins");
|
||||
let menu_segment = candidates
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|(_, p)| p.distance(insertion_at) > 30.)
|
||||
.expect("a distinct committed segment for the menu control");
|
||||
let start = screen(&canvas, insertion_at);
|
||||
bend_press(&canvas, &mut state, start);
|
||||
let away = start + Vector::new(45., 30.);
|
||||
motion(&canvas, &mut state, away, away);
|
||||
let preview = state.bend_preview.as_ref().unwrap();
|
||||
assert_eq!(preview.waypoints.len(), 3);
|
||||
assert_eq!(
|
||||
&preview.waypoints[1..],
|
||||
original.waypoints.as_slice(),
|
||||
"provisional insertion shifts both stored indices"
|
||||
);
|
||||
|
||||
// With the left gesture still active, moving to the last stored pin also
|
||||
// moves provisional pin0 there. The preview's first hit is therefore
|
||||
// pin0, but a menu built after cancellation must identify stored pin1.
|
||||
let menu_world = if click_pin {
|
||||
point(original.waypoints[1])
|
||||
} else {
|
||||
menu_segment.1
|
||||
};
|
||||
let menu_window = screen(&canvas, menu_world);
|
||||
motion(&canvas, &mut state, menu_window, menu_window);
|
||||
assert!(state.bend_preview.is_some());
|
||||
let menu = input(
|
||||
&canvas,
|
||||
&mut state,
|
||||
mouse::Event::ButtonPressed(mouse::Button::Right),
|
||||
menu_window,
|
||||
)
|
||||
.unwrap();
|
||||
let Event::ContextMenu {
|
||||
context,
|
||||
object,
|
||||
bend,
|
||||
route_segment,
|
||||
..
|
||||
} = menu
|
||||
else {
|
||||
panic!("expected context menu after cancelling drag")
|
||||
};
|
||||
assert_eq!(context, canvas.context);
|
||||
assert_eq!(object, relation);
|
||||
assert!(state.bend_preview.is_none());
|
||||
assert_eq!(
|
||||
canvas.scene(&state).unwrap().routed.edges[&relation].points,
|
||||
edge.points
|
||||
);
|
||||
if click_pin {
|
||||
assert_eq!(
|
||||
bend,
|
||||
Some(1),
|
||||
"menu index must address the stored route, not the provisional route"
|
||||
);
|
||||
assert!(route_segment.is_none());
|
||||
let remove = Event::RemoveBend {
|
||||
context,
|
||||
relation,
|
||||
index: bend.unwrap(),
|
||||
};
|
||||
let plan = state_edit::plan(&f.project, context, &remove).unwrap();
|
||||
let after = prepare_command(&f.project, &plan.command).unwrap().project;
|
||||
let mut expected = f.project.clone();
|
||||
expected.diagrams.get_mut(&f.diagram).unwrap().routes[0]
|
||||
.waypoints
|
||||
.remove(1);
|
||||
assert_eq!(
|
||||
after, expected,
|
||||
"remove menu action must delete exactly the addressed original pin"
|
||||
);
|
||||
} else {
|
||||
assert!(
|
||||
bend.is_none(),
|
||||
"provisional pin under cursor must not leak into menu"
|
||||
);
|
||||
let (segment, at) = route_segment.expect("committed route segment remains actionable");
|
||||
assert_eq!(segment, menu_segment.0);
|
||||
assert!(point(at).distance(menu_world) < 0.001);
|
||||
}
|
||||
assert!(
|
||||
input(
|
||||
&canvas,
|
||||
&mut state,
|
||||
mouse::Event::ButtonReleased(mouse::Button::Left),
|
||||
menu_window
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
assert_eq!(f.project.diagrams[&f.diagram].routes[0], original);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_bends_draw_before_update_excludes_preview_after_camera_or_bounds_change() {
|
||||
for change in 0..4 {
|
||||
let (f, relation) = bend_fixture(true);
|
||||
let mut canvas = canvas(&f, Tool::Select, 1.);
|
||||
canvas.selected = relation;
|
||||
let mut state = State::default();
|
||||
let baseline = canvas.scene_with_bounds(&state, Some(bounds())).unwrap();
|
||||
let start = screen(&canvas, point(baseline.routed.edges[&relation].pins[0]));
|
||||
let release = start + Vector::new(45., 30.);
|
||||
bend_press(&canvas, &mut state, start);
|
||||
motion(&canvas, &mut state, release, release);
|
||||
let displayed_preview = canvas.scene_with_bounds(&state, Some(bounds())).unwrap();
|
||||
assert_ne!(
|
||||
displayed_preview.routed.edges[&relation].pins,
|
||||
baseline.routed.edges[&relation].pins
|
||||
);
|
||||
let mut changed_bounds = bounds();
|
||||
match change {
|
||||
0 => canvas.zoom = 2.,
|
||||
1 => canvas.pan.y += 25.,
|
||||
2 => changed_bounds.x += 35.,
|
||||
_ => changed_bounds.width -= 90.,
|
||||
}
|
||||
// No update event between camera/layout change and this draw-time read.
|
||||
let shown = canvas
|
||||
.scene_with_bounds(&state, Some(changed_bounds))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
shown.routed.edges[&relation].pins, baseline.routed.edges[&relation].pins,
|
||||
"stale draw-time pins for case {change}"
|
||||
);
|
||||
assert_eq!(
|
||||
shown.routed.edges[&relation].points, baseline.routed.edges[&relation].points,
|
||||
"stale cached preview for case {change}"
|
||||
);
|
||||
let event = input_at_bounds(
|
||||
&canvas,
|
||||
&mut state,
|
||||
mouse::Event::ButtonReleased(mouse::Button::Left),
|
||||
release,
|
||||
changed_bounds,
|
||||
);
|
||||
assert!(!matches!(event, Some(Event::SetBends { .. })));
|
||||
assert!(state.bend_preview.is_none());
|
||||
}
|
||||
}
|
||||
@@ -43,12 +43,8 @@ pub struct State {
|
||||
press: Option<Press>,
|
||||
source: Option<Id>,
|
||||
preview: Option<(Id, Position)>,
|
||||
attachment_preview: Option<AttachmentPreview>,
|
||||
bend: Option<BendGesture>,
|
||||
bend_preview: Option<EdgeRoute>,
|
||||
pan_from: Option<(mouse::Button, Point)>,
|
||||
viewport: Option<Size>,
|
||||
attachment_viewport: Option<(f32, Vector, Rectangle)>,
|
||||
scene: RefCell<Option<CachedScene>>,
|
||||
geometry: canvas::Cache,
|
||||
paint_key: RefCell<Option<PaintKey>>,
|
||||
@@ -80,30 +76,6 @@ enum Press {
|
||||
/// Handles require a drag; an explicitly chosen reconnect tool accepts a click.
|
||||
start: Option<Point>,
|
||||
},
|
||||
Attachment {
|
||||
relation: Id,
|
||||
end: EdgeEnd,
|
||||
placement: Id,
|
||||
start: Point,
|
||||
},
|
||||
Bend {
|
||||
relation: Id,
|
||||
},
|
||||
}
|
||||
|
||||
struct BendGesture {
|
||||
start: Point,
|
||||
route: EdgeRoute,
|
||||
index: usize,
|
||||
baseline: Arc<Scene>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
struct AttachmentPreview {
|
||||
relation: Id,
|
||||
end: EdgeEnd,
|
||||
placement: Id,
|
||||
attachment: EdgeAttachment,
|
||||
}
|
||||
|
||||
struct Scene {
|
||||
@@ -114,16 +86,12 @@ struct Scene {
|
||||
struct CachedScene {
|
||||
context: Context,
|
||||
preview: Option<(Id, Position)>,
|
||||
attachment_preview: Option<AttachmentPreview>,
|
||||
bend_preview: Option<EdgeRoute>,
|
||||
scene: Arc<Scene>,
|
||||
}
|
||||
#[derive(PartialEq)]
|
||||
struct PaintKey {
|
||||
context: Context,
|
||||
preview: Option<(Id, Position)>,
|
||||
attachment_preview: Option<AttachmentPreview>,
|
||||
bend_preview: Option<EdgeRoute>,
|
||||
selected: Id,
|
||||
zoom: f32,
|
||||
pan: Vector,
|
||||
@@ -136,16 +104,9 @@ enum Hit {
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn cancel_edit(&mut self) {
|
||||
fn cancel_gesture(&mut self) {
|
||||
self.press = None;
|
||||
self.preview = None;
|
||||
self.attachment_preview = None;
|
||||
self.bend = None;
|
||||
self.bend_preview = None;
|
||||
self.attachment_viewport = None;
|
||||
}
|
||||
fn cancel_gesture(&mut self) {
|
||||
self.cancel_edit();
|
||||
self.pan_from = None;
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
@@ -688,36 +649,20 @@ impl StateCanvas<'_> {
|
||||
&& state_edit::validate_context(self.project, self.context, self.context).is_ok()
|
||||
}
|
||||
fn scene(&self, state: &State) -> Option<Arc<Scene>> {
|
||||
self.scene_with_bounds(state, None)
|
||||
}
|
||||
fn scene_with_bounds(&self, state: &State, bounds: Option<Rectangle>) -> Option<Arc<Scene>> {
|
||||
if !self.valid() {
|
||||
return None;
|
||||
}
|
||||
let route_active = self.gesture_active(state) && self.route_viewport_current(state, bounds);
|
||||
let preview = self
|
||||
.gesture_active(state)
|
||||
.then_some(state.preview)
|
||||
.flatten();
|
||||
let attachment_preview = route_active.then_some(state.attachment_preview).flatten();
|
||||
let bend_preview = route_active.then(|| state.bend_preview.clone()).flatten();
|
||||
if let Some(cached) = state.scene.borrow().as_ref()
|
||||
&& cached.context == self.context
|
||||
&& cached.preview == preview
|
||||
&& cached.attachment_preview == attachment_preview
|
||||
&& cached.bend_preview == bend_preview
|
||||
{
|
||||
return Some(cached.scene.clone());
|
||||
}
|
||||
let mut diagram = self.project.diagrams.get(&self.diagram)?.clone();
|
||||
if let Some(route) = &bend_preview {
|
||||
crate::route_edit::with_route(&mut diagram, route.clone());
|
||||
}
|
||||
if let Some(preview) = attachment_preview {
|
||||
let mut route = crate::route_edit::stored_route(&diagram, preview.relation);
|
||||
route.set_attachment(preview.end, Some(preview.attachment));
|
||||
crate::route_edit::with_route(&mut diagram, route);
|
||||
}
|
||||
if let Some((id, at)) = preview
|
||||
&& let Some(node) = diagram.nodes.get(&id)
|
||||
{
|
||||
@@ -742,17 +687,7 @@ impl StateCanvas<'_> {
|
||||
}
|
||||
}
|
||||
let view = projection::project_diagram(self.project, &diagram);
|
||||
let routed = if let Some(gesture) = state.bend.as_ref().filter(|_| bend_preview.is_some()) {
|
||||
routing::route_edge_preview(
|
||||
self.project,
|
||||
&diagram,
|
||||
&view,
|
||||
&gesture.baseline.routed,
|
||||
gesture.route.edge,
|
||||
)
|
||||
} else {
|
||||
routing::route_diagram(self.project, &diagram, &view)
|
||||
};
|
||||
let routed = routing::route_diagram(self.project, &diagram, &view);
|
||||
let symbol_labels = symbol_labels(self.project, &view, &routed);
|
||||
let scene = Arc::new(Scene {
|
||||
view,
|
||||
@@ -762,8 +697,6 @@ impl StateCanvas<'_> {
|
||||
*state.scene.borrow_mut() = Some(CachedScene {
|
||||
context: self.context,
|
||||
preview,
|
||||
attachment_preview,
|
||||
bend_preview,
|
||||
scene: scene.clone(),
|
||||
});
|
||||
Some(scene)
|
||||
@@ -895,84 +828,6 @@ impl StateCanvas<'_> {
|
||||
.min_by(|a, b| a.1.total_cmp(&b.1))
|
||||
.map(|(endpoint, _)| (route.id, endpoint))
|
||||
}
|
||||
fn bend_at(&self, scene: &Scene, world: Point) -> Option<usize> {
|
||||
let edge = scene.routed.edges.get(&self.selected)?;
|
||||
if edge.custom_suspended {
|
||||
return None;
|
||||
}
|
||||
edge.pins
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, pin)| {
|
||||
let distance = world.distance(point(*pin));
|
||||
(distance <= 8. / self.scale()).then_some((index, distance))
|
||||
})
|
||||
.min_by(|a, b| a.1.total_cmp(&b.1).then(a.0.cmp(&b.0)))
|
||||
.map(|(index, _)| index)
|
||||
}
|
||||
/// Preserve the rendered traversal index; overlapping runs can have the
|
||||
/// same world point but different positions among the authored pins.
|
||||
fn segment_at(&self, scene: &Scene, relation: Id, world: Point) -> Option<(usize, Position)> {
|
||||
let edge = scene.routed.edges.get(&relation)?;
|
||||
if edge.custom_suspended {
|
||||
return None;
|
||||
}
|
||||
edge.points
|
||||
.windows(2)
|
||||
.enumerate()
|
||||
.filter(|(index, _)| *index > 0 && index + 2 < edge.points.len())
|
||||
.filter_map(|(index, span)| {
|
||||
let projection = routing::project_onto_polyline(span, position(world))?;
|
||||
(projection.distance <= 7. / self.scale()).then_some((index, projection.distance))
|
||||
})
|
||||
.min_by(|a, b| a.1.total_cmp(&b.1).then(a.0.cmp(&b.0)))
|
||||
.map(|(index, _)| (index, position(world)))
|
||||
}
|
||||
fn begin_bend(
|
||||
&self,
|
||||
state: &mut State,
|
||||
scene: Arc<Scene>,
|
||||
window: Point,
|
||||
route: EdgeRoute,
|
||||
index: usize,
|
||||
bounds: Rectangle,
|
||||
) {
|
||||
state.press = Some(Press::Bend {
|
||||
relation: route.edge,
|
||||
});
|
||||
state.attachment_viewport = Some((self.zoom, self.pan, bounds));
|
||||
state.bend = Some(BendGesture {
|
||||
start: window,
|
||||
route,
|
||||
index,
|
||||
baseline: scene,
|
||||
});
|
||||
state.bend_preview = None;
|
||||
}
|
||||
fn move_bend_pointer(&self, state: &mut State, window: Point, local: Point) {
|
||||
let Some(gesture) = &state.bend else {
|
||||
return;
|
||||
};
|
||||
if window.distance(gesture.start) < 3. {
|
||||
// Also discard a provisional insertion. The exact original route
|
||||
// comes from the model, so an off-grid pin is never quantized here.
|
||||
state.bend_preview = None;
|
||||
return;
|
||||
}
|
||||
let world = self.world(local);
|
||||
if !world.x.is_finite() || !world.y.is_finite() {
|
||||
state.cancel_gesture();
|
||||
return;
|
||||
}
|
||||
let mut changed = gesture.route.clone();
|
||||
changed.waypoints[gesture.index] = Position {
|
||||
x: (world.x / 5.).round() * 5.,
|
||||
y: (world.y / 5.).round() * 5.,
|
||||
};
|
||||
let original =
|
||||
crate::route_edit::stored_route(&self.project.diagrams[&self.diagram], changed.edge);
|
||||
state.bend_preview = (changed != original).then_some(changed);
|
||||
}
|
||||
fn owner_at(&self, scene: &Scene, world: Point, kind: Kind) -> Option<Id> {
|
||||
self.node_at(scene, world)
|
||||
.filter(|n| state_edit::eligible_owner(self.project, self.diagram, n.element, kind))
|
||||
@@ -1001,98 +856,12 @@ impl StateCanvas<'_> {
|
||||
}
|
||||
}
|
||||
fn gesture_active(&self, state: &State) -> bool {
|
||||
self.editable
|
||||
&& state.context == Some(self.context)
|
||||
&& state.tool == Some(self.tool)
|
||||
&& state
|
||||
.bend
|
||||
.as_ref()
|
||||
.is_none_or(|g| g.route.edge == self.selected)
|
||||
}
|
||||
fn route_viewport_current(&self, state: &State, bounds: Option<Rectangle>) -> bool {
|
||||
state
|
||||
.attachment_viewport
|
||||
.is_none_or(|(zoom, pan, captured)| {
|
||||
zoom == self.zoom
|
||||
&& pan == self.pan
|
||||
&& bounds.is_none_or(|current| current == captured)
|
||||
})
|
||||
}
|
||||
fn move_attachment_pointer(&self, state: &mut State, window: Point, local: Point) {
|
||||
let Some(Press::Attachment {
|
||||
relation,
|
||||
end,
|
||||
placement,
|
||||
start,
|
||||
}) = state.press
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if window.distance(start) < 3. {
|
||||
// Returning to the press restores the exact persisted optional value,
|
||||
// without quantizing an existing attachment or pinning an automatic one.
|
||||
state.attachment_preview = None;
|
||||
return;
|
||||
}
|
||||
let Some(owner) = state_edit::attachment_node(self.project, self.diagram, relation, end)
|
||||
.filter(|owner| owner.id == placement)
|
||||
else {
|
||||
state.cancel_gesture();
|
||||
return;
|
||||
};
|
||||
let diagram = &self.project.diagrams[&self.diagram];
|
||||
let previous = state
|
||||
.attachment_preview
|
||||
.map(|preview| preview.attachment.side)
|
||||
.or_else(|| {
|
||||
diagram
|
||||
.routes
|
||||
.iter()
|
||||
.find(|route| route.edge == relation)
|
||||
.and_then(|route| route.attachment(end))
|
||||
.map(|attachment| attachment.side)
|
||||
});
|
||||
let world = self.world(local);
|
||||
if !world.x.is_finite() || !world.y.is_finite() {
|
||||
state.cancel_gesture();
|
||||
return;
|
||||
}
|
||||
let attachment =
|
||||
routing::attachment_from_point(&owner, position(world), previous, 6. / self.scale());
|
||||
state.attachment_preview = Some(AttachmentPreview {
|
||||
relation,
|
||||
end,
|
||||
placement,
|
||||
attachment,
|
||||
});
|
||||
self.editable && state.context == Some(self.context) && state.tool == Some(self.tool)
|
||||
}
|
||||
fn guidance(&self, state: &State, scene: &Scene, world: Option<Point>) -> String {
|
||||
if !self.editable {
|
||||
return "Pending edits · apply or cancel them before editing the diagram".into();
|
||||
}
|
||||
if state.bend.is_some()
|
||||
&& self.gesture_active(state)
|
||||
&& self.route_viewport_current(state, None)
|
||||
{
|
||||
return "Move bend · release to keep · Esc cancels".into();
|
||||
}
|
||||
if self.tool == Tool::Select
|
||||
&& state.press.is_none()
|
||||
&& scene
|
||||
.routed
|
||||
.edges
|
||||
.get(&self.selected)
|
||||
.is_some_and(|edge| !edge.custom_suspended)
|
||||
{
|
||||
return "Drag line to add a bend · squares move bends · right-click a square to remove"
|
||||
.into();
|
||||
}
|
||||
if let Tool::AdjustAttachment { relation, end } = self.tool {
|
||||
return format!(
|
||||
"Adjust {end:?} attachment of {} · drag that tip around its existing vertex boundary · Esc cancels · relation unchanged",
|
||||
self.project.named(relation)
|
||||
);
|
||||
}
|
||||
let hovered = world.and_then(|p| self.vertex_at(scene, p));
|
||||
let press = self.gesture_active(state).then_some(state.press).flatten();
|
||||
let source = self.gesture_active(state).then_some(state.source).flatten();
|
||||
@@ -1181,24 +950,10 @@ impl canvas::Program<Event> for StateCanvas<'_> {
|
||||
state.tool = Some(self.tool);
|
||||
}
|
||||
if !self.editable {
|
||||
// Pending edits block model changes, but read-only panning remains
|
||||
// available and must survive successive pointer events.
|
||||
state.cancel_edit();
|
||||
state.press = None;
|
||||
state.preview = None;
|
||||
state.source = None;
|
||||
}
|
||||
if state
|
||||
.bend
|
||||
.as_ref()
|
||||
.is_some_and(|g| g.route.edge != self.selected)
|
||||
{
|
||||
state.cancel_gesture();
|
||||
}
|
||||
if state
|
||||
.attachment_viewport
|
||||
.is_some_and(|view| view != (self.zoom, self.pan, bounds))
|
||||
{
|
||||
state.cancel_gesture();
|
||||
}
|
||||
// Winit positions are ordered events. The cursor supplied by Iced is the
|
||||
// final cursor for an entire batch and cannot reinterpret earlier clicks.
|
||||
let cursor = match event {
|
||||
@@ -1208,9 +963,6 @@ impl canvas::Program<Event> for StateCanvas<'_> {
|
||||
}
|
||||
canvas::Event::Mouse(mouse::Event::CursorLeft) => {
|
||||
state.pointer = None;
|
||||
if state.bend.is_some() {
|
||||
state.cancel_gesture();
|
||||
}
|
||||
cursor
|
||||
}
|
||||
canvas::Event::Mouse(
|
||||
@@ -1229,11 +981,6 @@ impl canvas::Program<Event> for StateCanvas<'_> {
|
||||
state.reset();
|
||||
return None;
|
||||
}
|
||||
canvas::Event::Window(iced::window::Event::Resized(_)) => {
|
||||
state.cancel_gesture();
|
||||
state.viewport = None;
|
||||
return None;
|
||||
}
|
||||
_ => cursor,
|
||||
};
|
||||
let window = cursor.position();
|
||||
@@ -1306,23 +1053,10 @@ impl canvas::Program<Event> for StateCanvas<'_> {
|
||||
}
|
||||
if *button == mouse::Button::Right {
|
||||
state.reset();
|
||||
// A canceled provisional pin shifts indices in its preview.
|
||||
// Menu commands must target the committed route instead.
|
||||
let scene = self.scene(state)?;
|
||||
let bend = self.bend_at(&scene, world);
|
||||
let (object, placement) = if bend.is_some() {
|
||||
(self.selected, None)
|
||||
} else {
|
||||
match self.hit(&scene, world) {
|
||||
Some(Hit::Node { element, placement }) => (element, Some(placement)),
|
||||
Some(Hit::Transition(id) | Hit::StateAction(id)) => (id, None),
|
||||
None => (self.diagram, None),
|
||||
}
|
||||
};
|
||||
let route_segment = if bend.is_none() {
|
||||
self.segment_at(&scene, object, world)
|
||||
} else {
|
||||
None
|
||||
let (object, placement) = match self.hit(&scene, world) {
|
||||
Some(Hit::Node { element, placement }) => (element, Some(placement)),
|
||||
Some(Hit::Transition(id) | Hit::StateAction(id)) => (id, None),
|
||||
None => (self.diagram, None),
|
||||
};
|
||||
return Some(
|
||||
Action::publish(Event::ContextMenu {
|
||||
@@ -1330,8 +1064,6 @@ impl canvas::Program<Event> for StateCanvas<'_> {
|
||||
object,
|
||||
placement,
|
||||
anchor: window,
|
||||
bend,
|
||||
route_segment,
|
||||
})
|
||||
.and_capture(),
|
||||
);
|
||||
@@ -1349,14 +1081,6 @@ impl canvas::Program<Event> for StateCanvas<'_> {
|
||||
});
|
||||
return Some(Action::capture());
|
||||
}
|
||||
if let Some(index) = self.bend_at(&scene, world) {
|
||||
let route = crate::route_edit::stored_route(
|
||||
&self.project.diagrams[&self.diagram],
|
||||
self.selected,
|
||||
);
|
||||
self.begin_bend(state, scene.clone(), window, route, index, bounds);
|
||||
return Some(Action::capture());
|
||||
}
|
||||
match self.hit(&scene, world) {
|
||||
Some(Hit::Node { placement, element }) => {
|
||||
let original = self
|
||||
@@ -1377,36 +1101,6 @@ impl canvas::Program<Event> for StateCanvas<'_> {
|
||||
)
|
||||
}
|
||||
Some(Hit::Transition(id) | Hit::StateAction(id)) => {
|
||||
if id == self.selected
|
||||
&& let Some(edge) = scene.routed.edges.get(&id)
|
||||
&& !Rectangle::new(
|
||||
point(edge.label.bounds.position),
|
||||
Size::new(
|
||||
edge.label.bounds.size.width,
|
||||
edge.label.bounds.size.height,
|
||||
),
|
||||
)
|
||||
.contains(world)
|
||||
&& let Some((segment, at)) = self.segment_at(&scene, id, world)
|
||||
{
|
||||
let mut route = crate::route_edit::stored_route(
|
||||
&self.project.diagrams[&self.diagram],
|
||||
id,
|
||||
);
|
||||
if let Some(index) = crate::route_edit::insert_pin_on_segment(
|
||||
&mut route, edge, segment, at,
|
||||
) {
|
||||
self.begin_bend(
|
||||
state,
|
||||
scene.clone(),
|
||||
window,
|
||||
route,
|
||||
index,
|
||||
bounds,
|
||||
);
|
||||
return Some(Action::capture());
|
||||
}
|
||||
}
|
||||
Some(Action::publish(Event::Select(self.context, id)).and_capture())
|
||||
}
|
||||
None => {
|
||||
@@ -1448,26 +1142,6 @@ impl canvas::Program<Event> for StateCanvas<'_> {
|
||||
});
|
||||
Some(Action::capture())
|
||||
}
|
||||
Tool::AdjustAttachment { relation, end } => {
|
||||
let owner =
|
||||
state_edit::attachment_node(self.project, self.diagram, relation, end)?;
|
||||
let route = scene.routed.edges.get(&relation)?;
|
||||
let tip = match end {
|
||||
EdgeEnd::Source => route.points.first(),
|
||||
EdgeEnd::Target => route.points.last(),
|
||||
}?;
|
||||
if world.distance(point(*tip)) > 10. / self.scale() {
|
||||
return None;
|
||||
}
|
||||
state.attachment_viewport = Some((self.zoom, self.pan, bounds));
|
||||
state.press = Some(Press::Attachment {
|
||||
relation,
|
||||
end,
|
||||
placement: owner.id,
|
||||
start: window,
|
||||
});
|
||||
Some(Action::capture())
|
||||
}
|
||||
}
|
||||
}
|
||||
canvas::Event::Mouse(mouse::Event::CursorMoved { .. }) => {
|
||||
@@ -1482,10 +1156,6 @@ impl canvas::Program<Event> for StateCanvas<'_> {
|
||||
}
|
||||
}
|
||||
self.move_pointer(state, window);
|
||||
if let Some(local) = local {
|
||||
self.move_attachment_pointer(state, window, local);
|
||||
self.move_bend_pointer(state, window, local);
|
||||
}
|
||||
Some(Action::request_redraw())
|
||||
}
|
||||
canvas::Event::Mouse(mouse::Event::ButtonReleased(button)) => {
|
||||
@@ -1506,49 +1176,16 @@ impl canvas::Program<Event> for StateCanvas<'_> {
|
||||
}
|
||||
if let Some(window) = window {
|
||||
self.move_pointer(state, window);
|
||||
if let Some(local) = local {
|
||||
self.move_attachment_pointer(state, window, local);
|
||||
self.move_bend_pointer(state, window, local);
|
||||
}
|
||||
}
|
||||
let press = state.press.take()?;
|
||||
state.attachment_viewport = None;
|
||||
let preview = state.preview.take();
|
||||
let attachment_preview = state.attachment_preview.take();
|
||||
let bend_preview = state.bend_preview.take();
|
||||
state.bend = None;
|
||||
let world = local.map(|p| self.world(p));
|
||||
let event = match press {
|
||||
Press::Bend { relation } if inside => bend_preview
|
||||
.filter(|route| route.edge == relation)
|
||||
.map(|route| Event::SetBends {
|
||||
context: self.context,
|
||||
relation,
|
||||
waypoints: route.waypoints,
|
||||
}),
|
||||
Press::Move { node, .. } => preview.map(|(_, position)| Event::Move {
|
||||
context: self.context,
|
||||
node,
|
||||
position,
|
||||
}),
|
||||
Press::Attachment {
|
||||
relation,
|
||||
end,
|
||||
placement,
|
||||
..
|
||||
} if inside => attachment_preview
|
||||
.filter(|preview| {
|
||||
preview.relation == relation
|
||||
&& preview.end == end
|
||||
&& preview.placement == placement
|
||||
})
|
||||
.map(|preview| Event::SetAttachment {
|
||||
context: self.context,
|
||||
relation,
|
||||
end,
|
||||
placement,
|
||||
attachment: Some(preview.attachment),
|
||||
}),
|
||||
Press::Create { owner, kind, start }
|
||||
if inside
|
||||
&& window?.distance(start) < 3.
|
||||
@@ -1636,19 +1273,15 @@ impl canvas::Program<Event> for StateCanvas<'_> {
|
||||
bounds: Rectangle,
|
||||
cursor: mouse::Cursor,
|
||||
) -> Vec<canvas::Geometry> {
|
||||
let Some(scene) = self.scene_with_bounds(state, Some(bounds)) else {
|
||||
let Some(scene) = self.scene(state) else {
|
||||
return vec![];
|
||||
};
|
||||
let route_active =
|
||||
self.gesture_active(state) && self.route_viewport_current(state, Some(bounds));
|
||||
let key = PaintKey {
|
||||
context: self.context,
|
||||
preview: self
|
||||
.gesture_active(state)
|
||||
.then_some(state.preview)
|
||||
.flatten(),
|
||||
attachment_preview: route_active.then_some(state.attachment_preview).flatten(),
|
||||
bend_preview: route_active.then(|| state.bend_preview.clone()).flatten(),
|
||||
selected: self.selected,
|
||||
zoom: self.scale(),
|
||||
pan: self.pan,
|
||||
@@ -1728,22 +1361,6 @@ impl canvas::Program<Event> for StateCanvas<'_> {
|
||||
style::MUTED,
|
||||
);
|
||||
if active {
|
||||
if !edge.custom_suspended && self.tool == Tool::Select {
|
||||
for pin in &edge.pins {
|
||||
let radius = 4. / self.scale();
|
||||
let handle = Path::rectangle(
|
||||
point(*pin) - Vector::new(radius, radius),
|
||||
Size::new(radius * 2., radius * 2.),
|
||||
);
|
||||
frame.fill(&handle, Color::WHITE);
|
||||
frame.stroke(
|
||||
&handle,
|
||||
Stroke::default()
|
||||
.with_color(style::TEAL)
|
||||
.with_width(1.6 / self.scale()),
|
||||
);
|
||||
}
|
||||
}
|
||||
for end in [edge.points.first(), edge.points.last()]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
@@ -1774,20 +1391,6 @@ impl canvas::Program<Event> for StateCanvas<'_> {
|
||||
.with_width(2.5 / self.scale()),
|
||||
);
|
||||
}
|
||||
if let Tool::AdjustAttachment { relation, end } = self.tool
|
||||
&& let Some(route) = scene.routed.edges.get(&relation)
|
||||
&& let Some(tip) = match end {
|
||||
EdgeEnd::Source => route.points.first(),
|
||||
EdgeEnd::Target => route.points.last(),
|
||||
}
|
||||
{
|
||||
frame.stroke(
|
||||
&Path::circle(point(*tip), 9. / self.scale()),
|
||||
Stroke::default()
|
||||
.with_color(style::TEAL)
|
||||
.with_width(2. / self.scale()),
|
||||
);
|
||||
}
|
||||
if let Some(at) = world
|
||||
&& self.editable
|
||||
{
|
||||
@@ -2158,8 +1761,6 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::document::Document;
|
||||
|
||||
include!("state_bend_tests.rs");
|
||||
|
||||
struct Fixture {
|
||||
project: Project,
|
||||
diagram: Id,
|
||||
@@ -2615,7 +2216,7 @@ mod tests {
|
||||
motion(&canvas, &mut state, at, at);
|
||||
assert!(matches!(input(&canvas, &mut state,
|
||||
mouse::Event::ButtonPressed(mouse::Button::Right), at),
|
||||
Some(Event::ContextMenu { context, object, placement: Some(node), anchor, .. })
|
||||
Some(Event::ContextMenu { context, object, placement: Some(node), anchor })
|
||||
if context == canvas.context && object == f.b
|
||||
&& node == placement(&f, f.b) && anchor == at));
|
||||
let background = Point::new(
|
||||
@@ -2625,7 +2226,7 @@ mod tests {
|
||||
motion(&canvas, &mut state, background, background);
|
||||
assert!(matches!(input(&canvas, &mut state,
|
||||
mouse::Event::ButtonPressed(mouse::Button::Right), background),
|
||||
Some(Event::ContextMenu { context, object, placement: None, anchor, .. })
|
||||
Some(Event::ContextMenu { context, object, placement: None, anchor })
|
||||
if context == canvas.context && object == f.diagram && anchor == background));
|
||||
}
|
||||
|
||||
@@ -2882,242 +2483,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_attachment_drag_is_view_only_and_one_source_history_operation() {
|
||||
for end in [EdgeEnd::Source, EdgeEnd::Target] {
|
||||
for zoom in [0.5, 1., 2.] {
|
||||
let mut f = fixture();
|
||||
let relation =
|
||||
f.project
|
||||
.relate(RelationKind::StateTransition, "Go standby", f.a, f.b);
|
||||
let second = state_edit::create_view(&f.project, f.machine).unwrap();
|
||||
let other_id = second.diagram;
|
||||
f.project = prepare_command(&f.project, &second.command)
|
||||
.unwrap()
|
||||
.project;
|
||||
let other = f.project.diagrams[&other_id].clone();
|
||||
let mut canvas = canvas(&f, Tool::AdjustAttachment { relation, end }, zoom);
|
||||
canvas.selected = relation;
|
||||
let mut state = State::default();
|
||||
let scene = canvas.scene(&state).unwrap();
|
||||
let route = &scene.routed.edges[&relation];
|
||||
let tip = match end {
|
||||
EdgeEnd::Source => route.points.first(),
|
||||
EdgeEnd::Target => route.points.last(),
|
||||
}
|
||||
.unwrap();
|
||||
let handle = screen(&canvas, point(*tip));
|
||||
motion(&canvas, &mut state, handle, handle);
|
||||
input(
|
||||
&canvas,
|
||||
&mut state,
|
||||
mouse::Event::ButtonPressed(mouse::Button::Left),
|
||||
handle,
|
||||
);
|
||||
assert!(
|
||||
input(
|
||||
&canvas,
|
||||
&mut state,
|
||||
mouse::Event::ButtonReleased(mouse::Button::Left),
|
||||
handle
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
assert!(
|
||||
state.attachment_preview.is_none(),
|
||||
"A click must not materialize an automatic endpoint"
|
||||
);
|
||||
let owner =
|
||||
state_edit::attachment_node(&f.project, f.diagram, relation, end).unwrap();
|
||||
let destination = screen(
|
||||
&canvas,
|
||||
Point::new(owner.position.x + owner.size.width * 0.25, owner.position.y),
|
||||
);
|
||||
input(
|
||||
&canvas,
|
||||
&mut state,
|
||||
mouse::Event::ButtonPressed(mouse::Button::Left),
|
||||
handle,
|
||||
);
|
||||
motion(&canvas, &mut state, destination, destination);
|
||||
assert!(state.attachment_preview.is_some());
|
||||
assert!(f.project.diagrams[&f.diagram].routes.is_empty());
|
||||
let event = input(
|
||||
&canvas,
|
||||
&mut state,
|
||||
mouse::Event::ButtonReleased(mouse::Button::Left),
|
||||
destination,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(matches!(event, Event::SetAttachment { end: actual, .. } if actual == end));
|
||||
let plan = state_edit::plan(&f.project, canvas.context, &event).unwrap();
|
||||
let mut document =
|
||||
Document::from_project(f.project.clone(), Default::default()).unwrap();
|
||||
let source = document.bundle().clone();
|
||||
document.execute(plan.command).unwrap();
|
||||
assert_eq!(document.project().relations, f.project.relations);
|
||||
assert_eq!(document.project().diagrams[&other_id], other);
|
||||
let route = &document.project().diagrams[&f.diagram].routes[0];
|
||||
assert_eq!(
|
||||
route.attachment(end),
|
||||
Some(EdgeAttachment {
|
||||
side: PortAnchorSide::Top,
|
||||
offset: 0.25
|
||||
})
|
||||
);
|
||||
let edited = document.bundle().clone();
|
||||
assert!(document.undo());
|
||||
assert_eq!(document.bundle(), &source);
|
||||
assert!(!document.undo());
|
||||
assert!(document.redo());
|
||||
assert_eq!(document.bundle(), &edited);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attachment_return_to_press_restores_exact_automatic_or_fractional_original() {
|
||||
for original in [
|
||||
None,
|
||||
Some(EdgeAttachment {
|
||||
side: PortAnchorSide::Top,
|
||||
offset: 0.234567,
|
||||
}),
|
||||
] {
|
||||
for end in [EdgeEnd::Source, EdgeEnd::Target] {
|
||||
let mut f = fixture();
|
||||
let relation =
|
||||
f.project
|
||||
.relate(RelationKind::StateTransition, "Go standby", f.a, f.b);
|
||||
if original.is_some() {
|
||||
let mut route =
|
||||
crate::route_edit::stored_route(&f.project.diagrams[&f.diagram], relation);
|
||||
route.set_attachment(end, original);
|
||||
f.project
|
||||
.diagrams
|
||||
.get_mut(&f.diagram)
|
||||
.unwrap()
|
||||
.routes
|
||||
.push(route);
|
||||
}
|
||||
let canvas = canvas(&f, Tool::AdjustAttachment { relation, end }, 1.);
|
||||
let mut state = State::default();
|
||||
let before = canvas.scene(&state).unwrap();
|
||||
let edge = &before.routed.edges[&relation];
|
||||
let tip = match end {
|
||||
EdgeEnd::Source => edge.points.first(),
|
||||
EdgeEnd::Target => edge.points.last(),
|
||||
}
|
||||
.unwrap();
|
||||
let start = screen(&canvas, point(*tip));
|
||||
let away = start + Vector::new(80., 40.);
|
||||
motion(&canvas, &mut state, start, start);
|
||||
input(
|
||||
&canvas,
|
||||
&mut state,
|
||||
mouse::Event::ButtonPressed(mouse::Button::Left),
|
||||
start,
|
||||
);
|
||||
motion(&canvas, &mut state, away, away);
|
||||
assert!(state.attachment_preview.is_some());
|
||||
motion(&canvas, &mut state, start, start);
|
||||
assert!(state.attachment_preview.is_none());
|
||||
assert_eq!(
|
||||
canvas.scene(&state).unwrap().routed.edges[&relation].points,
|
||||
edge.points
|
||||
);
|
||||
assert!(
|
||||
input(
|
||||
&canvas,
|
||||
&mut state,
|
||||
mouse::Event::ButtonReleased(mouse::Button::Left),
|
||||
start
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
assert_eq!(
|
||||
crate::route_edit::stored_route(&f.project.diagrams[&f.diagram], relation)
|
||||
.attachment(end),
|
||||
original
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attachment_drag_cancels_on_camera_context_tool_pending_and_focus_changes() {
|
||||
let mut f = fixture();
|
||||
let relation = f
|
||||
.project
|
||||
.relate(RelationKind::StateTransition, "Go standby", f.a, f.b);
|
||||
for cancel in 0..9 {
|
||||
let mut canvas = canvas(
|
||||
&f,
|
||||
Tool::AdjustAttachment {
|
||||
relation,
|
||||
end: EdgeEnd::Source,
|
||||
},
|
||||
1.,
|
||||
);
|
||||
canvas.selected = relation;
|
||||
let mut state = State::default();
|
||||
let scene = canvas.scene(&state).unwrap();
|
||||
let handle = screen(&canvas, point(scene.routed.edges[&relation].points[0]));
|
||||
let destination = handle + Vector::new(40., 30.);
|
||||
motion(&canvas, &mut state, handle, handle);
|
||||
input(
|
||||
&canvas,
|
||||
&mut state,
|
||||
mouse::Event::ButtonPressed(mouse::Button::Left),
|
||||
handle,
|
||||
);
|
||||
motion(&canvas, &mut state, destination, destination);
|
||||
assert!(state.attachment_preview.is_some());
|
||||
match cancel {
|
||||
0 => canvas.zoom = 2.,
|
||||
1 => canvas.pan.x += 10.,
|
||||
2 => canvas.context.epoch += 1,
|
||||
3 => canvas.tool = Tool::Select,
|
||||
4 => canvas.editable = false,
|
||||
5 | 6 => {
|
||||
input(
|
||||
&canvas,
|
||||
&mut state,
|
||||
mouse::Event::ButtonPressed(if cancel == 5 {
|
||||
mouse::Button::Right
|
||||
} else {
|
||||
mouse::Button::Middle
|
||||
}),
|
||||
destination,
|
||||
);
|
||||
}
|
||||
7 => {
|
||||
canvas::Program::update(
|
||||
&canvas,
|
||||
&mut state,
|
||||
&canvas::Event::Window(iced::window::Event::Unfocused),
|
||||
bounds(),
|
||||
mouse::Cursor::Available(destination),
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
motion(&canvas, &mut state, Point::new(-100., -100.), destination);
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
input(
|
||||
&canvas,
|
||||
&mut state,
|
||||
mouse::Event::ButtonReleased(mouse::Button::Left),
|
||||
destination
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
assert!(state.attachment_preview.is_none());
|
||||
assert!(f.project.diagrams[&f.diagram].routes.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selected_endpoint_reconnect_preserves_identity_and_requires_handle_motion() {
|
||||
let mut f = fixture();
|
||||
|
||||
@@ -20,7 +20,6 @@ pub enum Tool {
|
||||
Region,
|
||||
Transition,
|
||||
Reconnect { relation: Id, endpoint: Endpoint },
|
||||
AdjustAttachment { relation: Id, end: EdgeEnd },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -39,8 +38,6 @@ pub enum Event {
|
||||
object: Id,
|
||||
placement: Option<Id>,
|
||||
anchor: iced::Point,
|
||||
bend: Option<usize>,
|
||||
route_segment: Option<(usize, Position)>,
|
||||
},
|
||||
CreateAt {
|
||||
context: Context,
|
||||
@@ -69,32 +66,6 @@ pub enum Event {
|
||||
endpoint: Endpoint,
|
||||
vertex: Id,
|
||||
},
|
||||
SetAttachment {
|
||||
context: Context,
|
||||
relation: Id,
|
||||
end: EdgeEnd,
|
||||
placement: Id,
|
||||
attachment: Option<EdgeAttachment>,
|
||||
},
|
||||
SetBends {
|
||||
context: Context,
|
||||
relation: Id,
|
||||
waypoints: Vec<Position>,
|
||||
},
|
||||
AddBend {
|
||||
context: Context,
|
||||
relation: Id,
|
||||
segment: Option<(usize, Position)>,
|
||||
},
|
||||
RemoveBend {
|
||||
context: Context,
|
||||
relation: Id,
|
||||
index: usize,
|
||||
},
|
||||
ClearBends {
|
||||
context: Context,
|
||||
relation: Id,
|
||||
},
|
||||
}
|
||||
|
||||
impl Event {
|
||||
@@ -113,12 +84,7 @@ impl Event {
|
||||
| Self::Place { context: c, .. }
|
||||
| Self::Move { context: c, .. }
|
||||
| Self::Connect { context: c, .. }
|
||||
| Self::Reconnect { context: c, .. }
|
||||
| Self::SetAttachment { context: c, .. }
|
||||
| Self::SetBends { context: c, .. }
|
||||
| Self::AddBend { context: c, .. }
|
||||
| Self::RemoveBend { context: c, .. }
|
||||
| Self::ClearBends { context: c, .. } => Some(*c),
|
||||
| Self::Reconnect { context: c, .. } => Some(*c),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -283,54 +249,6 @@ pub fn can_reconnect(
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
/// Only the actual visible transition owner can receive a visual attachment.
|
||||
/// Folded promotion, semantic ports and internal-transition rows are excluded.
|
||||
pub fn attachment_node(
|
||||
project: &Project,
|
||||
diagram: Id,
|
||||
relation: Id,
|
||||
end: EdgeEnd,
|
||||
) -> Option<projection::ProjectedNode> {
|
||||
machine(project, diagram).ok()?;
|
||||
let relation_record = project.relations.get(&relation)?;
|
||||
if relation_record.kind() != RelationKind::StateTransition
|
||||
|| oa_core::routing::semantic_port_endpoint(project, relation, end).is_some()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let view = projection::project_view(project, diagram);
|
||||
let edge = view.edges.iter().find(|edge| edge.id == relation)?;
|
||||
let stored = project.diagrams.get(&diagram)?;
|
||||
if projection::edge_endpoint_placements(project, stored, relation)?
|
||||
!= (edge.source, edge.target)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let owner = match end {
|
||||
EdgeEnd::Source => edge.source,
|
||||
EdgeEnd::Target => edge.target,
|
||||
};
|
||||
view.nodes.iter().find(|node| node.id == owner).cloned()
|
||||
}
|
||||
|
||||
/// Actual visible State transition geometry, never an internal row or folded proxy.
|
||||
pub fn bend_route(
|
||||
project: &Project,
|
||||
diagram: Id,
|
||||
relation: Id,
|
||||
) -> Option<oa_core::routing::RoutedEdge> {
|
||||
machine(project, diagram).ok()?;
|
||||
if project.relations.get(&relation)?.kind() != RelationKind::StateTransition {
|
||||
return None;
|
||||
}
|
||||
let stored = project.diagrams.get(&diagram)?;
|
||||
let view = projection::project_diagram(project, stored);
|
||||
let geometry = oa_core::routing::route_diagram(project, stored, &view)
|
||||
.edges
|
||||
.remove(&relation)?;
|
||||
(!geometry.custom_suspended).then_some(geometry)
|
||||
}
|
||||
|
||||
fn checked(project: &Project, plan: Plan) -> Result<Plan, ModelError> {
|
||||
prepare_command(project, &plan.command)?;
|
||||
Ok(plan)
|
||||
@@ -574,82 +492,6 @@ pub fn plan(project: &Project, current: Context, event: &Event) -> Result<Plan,
|
||||
relation,
|
||||
None,
|
||||
),
|
||||
Event::SetAttachment {
|
||||
relation,
|
||||
end,
|
||||
placement,
|
||||
attachment,
|
||||
..
|
||||
} => {
|
||||
attachment_node(project, diagram, relation, end)
|
||||
.filter(|owner| owner.id == placement)
|
||||
.ok_or_else(|| {
|
||||
error("Choose the actual visible transition endpoint in this view.")
|
||||
})?;
|
||||
let mut route = crate::route_edit::stored_route(&project.diagrams[&diagram], relation);
|
||||
route.set_attachment(end, attachment);
|
||||
(Command::SetEdgeRoute { diagram, route }, relation, None)
|
||||
}
|
||||
Event::SetBends { relation, .. }
|
||||
| Event::AddBend { relation, .. }
|
||||
| Event::RemoveBend { relation, .. }
|
||||
| Event::ClearBends { relation, .. } => {
|
||||
let geometry = bend_route(project, diagram, relation)
|
||||
.ok_or_else(|| error("Choose an actual visible transition route in this view."))?;
|
||||
let mut route = crate::route_edit::stored_route(&project.diagrams[&diagram], relation);
|
||||
match event {
|
||||
Event::SetBends { waypoints, .. } => {
|
||||
if waypoints.len() > 128 {
|
||||
return Err(error("A route supports at most 128 bends."));
|
||||
}
|
||||
for &point in waypoints {
|
||||
finite(point)?;
|
||||
}
|
||||
route.waypoints = waypoints.clone();
|
||||
}
|
||||
Event::AddBend { segment, .. } => {
|
||||
let (segment, point) = segment
|
||||
.or_else(|| {
|
||||
geometry
|
||||
.points
|
||||
.windows(2)
|
||||
.enumerate()
|
||||
.filter(|(index, _)| {
|
||||
*index > 0 && index + 2 < geometry.points.len()
|
||||
})
|
||||
.max_by(|(_, a), (_, b)| {
|
||||
let length = |s: &[Position]| {
|
||||
(s[1].x - s[0].x).abs() + (s[1].y - s[0].y).abs()
|
||||
};
|
||||
length(a).total_cmp(&length(b))
|
||||
})
|
||||
.map(|(index, span)| {
|
||||
(
|
||||
index,
|
||||
Position {
|
||||
x: (span[0].x + span[1].x) / 2.,
|
||||
y: (span[0].y + span[1].y) / 2.,
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
.ok_or_else(|| error("This route has no interior segment for a bend."))?;
|
||||
crate::route_edit::insert_pin_on_segment(&mut route, &geometry, segment, point)
|
||||
.ok_or_else(|| {
|
||||
error("The bend segment is stale, invalid, or at capacity.")
|
||||
})?;
|
||||
}
|
||||
Event::RemoveBend { index, .. } => {
|
||||
if *index >= route.waypoints.len() {
|
||||
return Err(error("This bend no longer exists."));
|
||||
}
|
||||
route.waypoints.remove(*index);
|
||||
}
|
||||
Event::ClearBends { .. } => route.waypoints.clear(),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
(Command::SetEdgeRoute { diagram, route }, relation, None)
|
||||
}
|
||||
_ => {
|
||||
return Err(error(
|
||||
"Handle this navigation or tool request without a model command.",
|
||||
@@ -738,85 +580,6 @@ mod tests {
|
||||
plan.diagram
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attachment_reset_preserves_other_end_pins_label_and_rejects_ineligible_owners() {
|
||||
let mut f = fixture();
|
||||
let diagram = create(&mut f);
|
||||
let c = context(diagram);
|
||||
let edge = f.project.relate(
|
||||
RelationKind::StateTransition,
|
||||
"Leave work",
|
||||
f.leaf,
|
||||
f.target,
|
||||
);
|
||||
let attachment = EdgeAttachment {
|
||||
side: PortAnchorSide::Top,
|
||||
offset: 0.3,
|
||||
};
|
||||
let mut route = crate::route_edit::stored_route(&f.project.diagrams[&diagram], edge);
|
||||
route.source_attachment = Some(attachment);
|
||||
route.target_attachment = Some(attachment);
|
||||
route.waypoints.push(Position { x: 700., y: 700. });
|
||||
// Retain the exact label value alongside the other route components.
|
||||
route.label = Some(EdgeLabel {
|
||||
fraction: 0.5,
|
||||
offset: Position { x: 10., y: 20. },
|
||||
});
|
||||
f.project
|
||||
.diagrams
|
||||
.get_mut(&diagram)
|
||||
.unwrap()
|
||||
.routes
|
||||
.push(route.clone());
|
||||
let owner = attachment_node(&f.project, diagram, edge, EdgeEnd::Source).unwrap();
|
||||
let event = Event::SetAttachment {
|
||||
context: c,
|
||||
relation: edge,
|
||||
end: EdgeEnd::Source,
|
||||
placement: owner.id,
|
||||
attachment: None,
|
||||
};
|
||||
let accepted = prepare_command(&f.project, &plan(&f.project, c, &event).unwrap().command)
|
||||
.unwrap()
|
||||
.project;
|
||||
route.source_attachment = None;
|
||||
assert_eq!(accepted.diagrams[&diagram].routes, vec![route]);
|
||||
assert_eq!(accepted.relations, f.project.relations);
|
||||
let wrong = Event::SetAttachment {
|
||||
context: c,
|
||||
relation: edge,
|
||||
end: EdgeEnd::Source,
|
||||
placement: node(&f.project, diagram, f.target),
|
||||
attachment: Some(attachment),
|
||||
};
|
||||
assert!(plan(&f.project, c, &wrong).is_err());
|
||||
let composite = node(&f.project, diagram, f.composite);
|
||||
f.project
|
||||
.diagrams
|
||||
.get_mut(&diagram)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&composite)
|
||||
.unwrap()
|
||||
.collapsed = true;
|
||||
assert!(attachment_node(&f.project, diagram, edge, EdgeEnd::Source).is_none());
|
||||
assert!(plan(&f.project, c, &event).is_err());
|
||||
let internal = f.project.relate(
|
||||
RelationKind::StateTransition,
|
||||
"Internal",
|
||||
f.target,
|
||||
f.target,
|
||||
);
|
||||
if let RelationBody::StateTransition {
|
||||
transition_kind, ..
|
||||
} = &mut f.project.relations.get_mut(&internal).unwrap().body
|
||||
{
|
||||
*transition_kind = TransitionKind::Internal;
|
||||
}
|
||||
assert!(attachment_node(&f.project, diagram, internal, EdgeEnd::Source).is_none());
|
||||
assert!(attachment_node(&f.project, diagram, internal, EdgeEnd::Target).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn view_initializes_only_its_machine_in_one_source_and_model_history_entry() {
|
||||
let f = fixture();
|
||||
@@ -1000,8 +763,6 @@ mod tests {
|
||||
.routes
|
||||
.push(EdgeRoute {
|
||||
edge,
|
||||
source_attachment: None,
|
||||
target_attachment: None,
|
||||
waypoints: vec![pin],
|
||||
label: None,
|
||||
});
|
||||
|
||||
@@ -161,53 +161,6 @@ pub fn popup(app: &App, opened: &Opened, clean: bool) -> Option<Menu<Message>> {
|
||||
);
|
||||
entries.push(item("Edit transition", edit, true));
|
||||
if let Some(context) = context {
|
||||
let bend_geometry = state_edit::bend_route(project, context.diagram, id);
|
||||
let bend_count = bend_geometry.as_ref().map_or(0, |route| route.pins.len());
|
||||
let can_add = bend_geometry.as_ref().is_some_and(|route| {
|
||||
bend_count < 128
|
||||
&& request.route_segment.map_or_else(
|
||||
|| route.points.len() >= 4,
|
||||
|(index, point)| {
|
||||
index
|
||||
.checked_add(2)
|
||||
.is_some_and(|end| end <= route.points.len())
|
||||
&& point.x.is_finite()
|
||||
&& point.y.is_finite()
|
||||
},
|
||||
)
|
||||
});
|
||||
entries.push(item(
|
||||
if request.route_segment.is_some() {
|
||||
"Add bend here"
|
||||
} else {
|
||||
"Add bend"
|
||||
},
|
||||
diagram_event(Event::AddBend {
|
||||
context,
|
||||
relation: id,
|
||||
segment: request.route_segment,
|
||||
}),
|
||||
can_add,
|
||||
));
|
||||
if let Some(index) = request.bend {
|
||||
entries.push(item(
|
||||
"Remove bend",
|
||||
diagram_event(Event::RemoveBend {
|
||||
context,
|
||||
relation: id,
|
||||
index,
|
||||
}),
|
||||
bend_geometry.is_some() && index < bend_count,
|
||||
));
|
||||
}
|
||||
entries.push(item(
|
||||
"Clear bends",
|
||||
diagram_event(Event::ClearBends {
|
||||
context,
|
||||
relation: id,
|
||||
}),
|
||||
bend_geometry.is_some() && bend_count > 0,
|
||||
));
|
||||
let visible = projection::project_view(project, context.diagram)
|
||||
.edges
|
||||
.iter()
|
||||
@@ -225,44 +178,6 @@ pub fn popup(app: &App, opened: &Opened, clean: bool) -> Option<Menu<Message>> {
|
||||
visible,
|
||||
));
|
||||
}
|
||||
for (caption, reset, end) in [
|
||||
(
|
||||
"Adjust source attachment",
|
||||
"Reset source attachment",
|
||||
EdgeEnd::Source,
|
||||
),
|
||||
(
|
||||
"Adjust target attachment",
|
||||
"Reset target attachment",
|
||||
EdgeEnd::Target,
|
||||
),
|
||||
] {
|
||||
let owner = state_edit::attachment_node(project, context.diagram, id, end);
|
||||
entries.push(item(
|
||||
caption,
|
||||
diagram_event(Event::Choose(Tool::AdjustAttachment { relation: id, end })),
|
||||
owner.is_some(),
|
||||
));
|
||||
if let Some(owner) = owner {
|
||||
let authored = project.diagrams[&context.diagram]
|
||||
.routes
|
||||
.iter()
|
||||
.find(|route| route.edge == id)
|
||||
.and_then(|route| route.attachment(end))
|
||||
.is_some();
|
||||
entries.push(item(
|
||||
reset,
|
||||
diagram_event(Event::SetAttachment {
|
||||
context,
|
||||
relation: id,
|
||||
end,
|
||||
placement: owner.id,
|
||||
attachment: None,
|
||||
}),
|
||||
authored,
|
||||
));
|
||||
}
|
||||
}
|
||||
if !visible {
|
||||
entries.push(Entry::title(
|
||||
"Use transition details for hidden or internal endpoints",
|
||||
|
||||
@@ -22,9 +22,6 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
#[cfg(test)]
|
||||
#[path = "diagram/attachment_tests.rs"]
|
||||
mod attachment_tests;
|
||||
#[cfg(test)]
|
||||
#[path = "diagram/lifecycle_tests.rs"]
|
||||
mod lifecycle_tests;
|
||||
#[cfg(test)]
|
||||
@@ -56,7 +53,6 @@ pub struct DiagramCanvas<'a> {
|
||||
pub pan: Vector,
|
||||
pub context: Context,
|
||||
pub selected_bend: Option<usize>,
|
||||
pub selected_attachment: Option<EdgeEnd>,
|
||||
pub editable: bool,
|
||||
pub placing: bool,
|
||||
pub connection: Option<&'a crate::connection_edit::Session>,
|
||||
@@ -71,7 +67,6 @@ pub struct State {
|
||||
pan_start: Option<(mouse::Button, Point)>,
|
||||
context: Option<Context>,
|
||||
route_drag: Option<Gesture>,
|
||||
attachment_viewport: Option<(f32, Vector, Rectangle)>,
|
||||
connection_press: Option<(Id, Point)>,
|
||||
connection_hover: Option<Point>,
|
||||
port_drag: Option<crate::port_edit::Gesture>,
|
||||
@@ -299,11 +294,6 @@ impl DiagramCanvas<'_> {
|
||||
);
|
||||
request.canvas = Some(self.context);
|
||||
request.creation_point = Some(position(world));
|
||||
if self.attachment_at(&scene, world).is_some() {
|
||||
request.object = self.selected;
|
||||
request.route_point = Some(position(world));
|
||||
return request;
|
||||
}
|
||||
if let Some(edge) = scene.routed.edges.get(&self.selected)
|
||||
&& !edge.custom_suspended
|
||||
&& let Some(index) = edge
|
||||
@@ -381,39 +371,6 @@ impl DiagramCanvas<'_> {
|
||||
})
|
||||
.map(|(id, segment, _)| (id, segment))
|
||||
}
|
||||
fn attachment_at<'b>(
|
||||
&self,
|
||||
scene: &'b Scene,
|
||||
world: Point,
|
||||
) -> Option<(EdgeEnd, &'b ProjectedNode)> {
|
||||
let edge = scene.routed.edges.get(&self.selected)?;
|
||||
if edge.custom_suspended {
|
||||
return None;
|
||||
}
|
||||
[
|
||||
(EdgeEnd::Source, edge.points.first()),
|
||||
(EdgeEnd::Target, edge.points.last()),
|
||||
]
|
||||
.into_iter()
|
||||
.filter_map(|(end, at)| {
|
||||
let distance = world.distance(point(*at?));
|
||||
let owner = route_edit::attachment_node(
|
||||
self.project,
|
||||
&self.project.diagrams[&self.diagram],
|
||||
&scene.view,
|
||||
self.selected,
|
||||
end,
|
||||
)?;
|
||||
(distance <= 8. / self.zoom).then_some((end, owner, distance))
|
||||
})
|
||||
.min_by(|a, b| {
|
||||
a.2.total_cmp(&b.2).then_with(|| {
|
||||
(Some(a.0) != self.selected_attachment)
|
||||
.cmp(&(Some(b.0) != self.selected_attachment))
|
||||
})
|
||||
})
|
||||
.map(|(end, owner, _)| (end, owner))
|
||||
}
|
||||
fn hit_label(&self, routed: &RoutedView, p: Point) -> Option<Id> {
|
||||
let p = self.world(p);
|
||||
routed
|
||||
@@ -604,24 +561,6 @@ impl canvas::Program<Message> for DiagramCanvas<'_> {
|
||||
bounds: Rectangle,
|
||||
cursor: mouse::Cursor,
|
||||
) -> Option<Action<Message>> {
|
||||
// An attachment belongs to the boundary geometry captured at press.
|
||||
// Camera/layout changes must not reinterpret its pointer in another space.
|
||||
if matches!(
|
||||
state.route_drag.as_ref().map(|gesture| &gesture.handle),
|
||||
Some(Handle::Attachment { .. })
|
||||
) && (state.attachment_viewport != Some((self.zoom, self.pan, bounds))
|
||||
|| matches!(
|
||||
event,
|
||||
canvas::Event::Window(
|
||||
iced::window::Event::Unfocused | iced::window::Event::Resized(_)
|
||||
) | canvas::Event::Mouse(mouse::Event::CursorLeft)
|
||||
))
|
||||
{
|
||||
state.route_drag = None;
|
||||
state.attachment_viewport = None;
|
||||
state.pointer = None;
|
||||
return Some(Action::request_redraw());
|
||||
}
|
||||
// A completed work click retires App's allocation epoch before the
|
||||
// retained canvas sees a redraw. Its already-idle press must not eat
|
||||
// the first Delete. A live or not-yet-dispatched press stays guarded.
|
||||
@@ -901,21 +840,6 @@ impl canvas::Program<Message> for DiagramCanvas<'_> {
|
||||
&self.project.diagrams[&self.diagram],
|
||||
self.selected,
|
||||
);
|
||||
if let Some((end, owner)) = self.attachment_at(&scene, world) {
|
||||
let original = route.attachment(end);
|
||||
self.begin_route(
|
||||
state,
|
||||
world,
|
||||
Handle::Attachment {
|
||||
end,
|
||||
owner: owner.clone(),
|
||||
original,
|
||||
},
|
||||
route,
|
||||
);
|
||||
state.attachment_viewport = Some((self.zoom, self.pan, bounds));
|
||||
return Some(Action::capture());
|
||||
}
|
||||
if let Some(index) = edge
|
||||
.pins
|
||||
.iter()
|
||||
@@ -1039,7 +963,7 @@ impl canvas::Program<Message> for DiagramCanvas<'_> {
|
||||
return Some(Action::request_redraw());
|
||||
}
|
||||
if let Some(gesture) = &mut state.route_drag {
|
||||
gesture.move_attachment_to(self.world(p), self.zoom);
|
||||
gesture.move_to(self.world(p), self.zoom);
|
||||
return Some(Action::request_redraw().and_capture());
|
||||
}
|
||||
if let Some((_, start, original)) = state.drag {
|
||||
@@ -1116,43 +1040,17 @@ impl canvas::Program<Message> for DiagramCanvas<'_> {
|
||||
return Some(Action::capture());
|
||||
}
|
||||
if let Some(mut gesture) = state.route_drag.take() {
|
||||
if matches!(gesture.handle, Handle::Attachment { .. }) {
|
||||
state.attachment_viewport = None;
|
||||
if !cursor.is_over(bounds) {
|
||||
return Some(Action::request_redraw().and_capture());
|
||||
}
|
||||
}
|
||||
// `local` uses the ordered pointer position at release,
|
||||
// before any later motion in the same native event batch.
|
||||
if let Some(p) = local {
|
||||
gesture.move_attachment_to(self.world(p), self.zoom);
|
||||
gesture.move_to(self.world(p), self.zoom);
|
||||
}
|
||||
if !gesture.changed {
|
||||
return Some(Action::request_redraw().and_capture());
|
||||
}
|
||||
if let Handle::Attachment { end, owner, .. } = &gesture.handle {
|
||||
if gesture.route
|
||||
== route_edit::stored_route(
|
||||
&self.project.diagrams[&self.diagram],
|
||||
self.selected,
|
||||
)
|
||||
{
|
||||
return Some(Action::request_redraw().and_capture());
|
||||
}
|
||||
return Some(
|
||||
Action::publish(Message::Route(RouteEvent::CommitAttachment(
|
||||
gesture.context,
|
||||
gesture.route,
|
||||
*end,
|
||||
owner.id,
|
||||
)))
|
||||
.and_capture(),
|
||||
);
|
||||
}
|
||||
let bend = match gesture.handle {
|
||||
Handle::Bend(index) => Some(index),
|
||||
Handle::Label { .. } => None,
|
||||
Handle::Attachment { .. } => unreachable!(),
|
||||
};
|
||||
return Some(
|
||||
Action::publish(Message::Route(RouteEvent::Commit(
|
||||
@@ -1196,15 +1094,6 @@ impl canvas::Program<Message> for DiagramCanvas<'_> {
|
||||
canvas::Event::Mouse(mouse::Event::WheelScrolled { delta })
|
||||
if cursor.is_over(bounds) =>
|
||||
{
|
||||
// Cancel before publishing Zoom: a release may arrive in this
|
||||
// same event batch, before App applies the new camera.
|
||||
if matches!(
|
||||
state.route_drag.as_ref().map(|gesture| &gesture.handle),
|
||||
Some(Handle::Attachment { .. })
|
||||
) {
|
||||
state.route_drag = None;
|
||||
state.attachment_viewport = None;
|
||||
}
|
||||
let y = match delta {
|
||||
mouse::ScrollDelta::Lines { y, .. } | mouse::ScrollDelta::Pixels { y, .. } => {
|
||||
*y
|
||||
@@ -1447,41 +1336,6 @@ impl canvas::Program<Message> for DiagramCanvas<'_> {
|
||||
if let Some(edge) = scene.routed.edges.get(&self.selected)
|
||||
&& !edge.custom_suspended
|
||||
{
|
||||
for (end, at) in [
|
||||
(EdgeEnd::Source, edge.points.first()),
|
||||
(EdgeEnd::Target, edge.points.last()),
|
||||
] {
|
||||
if let Some(at) = at
|
||||
&& route_edit::attachment_node(
|
||||
project,
|
||||
&project.diagrams[&self.diagram],
|
||||
view,
|
||||
self.selected,
|
||||
end,
|
||||
)
|
||||
.is_some()
|
||||
{
|
||||
let at = point(*at);
|
||||
let handle = Path::rectangle(
|
||||
at - Vector::new(5.5 / self.zoom, 5.5 / self.zoom),
|
||||
Size::new(11. / self.zoom, 11. / self.zoom),
|
||||
);
|
||||
f.fill(
|
||||
&handle,
|
||||
if self.selected_attachment == Some(end) {
|
||||
style::TEAL
|
||||
} else {
|
||||
Color::WHITE
|
||||
},
|
||||
);
|
||||
f.stroke(
|
||||
&handle,
|
||||
Stroke::default()
|
||||
.with_color(style::TEAL)
|
||||
.with_width(1.7 / self.zoom),
|
||||
);
|
||||
}
|
||||
}
|
||||
for (index, pin) in edge.pins.iter().enumerate() {
|
||||
let handle = Path::circle(point(*pin), 5.5 / self.zoom);
|
||||
f.fill(
|
||||
@@ -2188,7 +2042,6 @@ mod tests {
|
||||
epoch: 0,
|
||||
},
|
||||
selected_bend: None,
|
||||
selected_attachment: None,
|
||||
editable: true,
|
||||
placing: false,
|
||||
connection: None,
|
||||
@@ -2265,7 +2118,6 @@ mod tests {
|
||||
epoch: 0,
|
||||
},
|
||||
selected_bend: None,
|
||||
selected_attachment: None,
|
||||
editable: true,
|
||||
placing: false,
|
||||
connection: None,
|
||||
@@ -2391,7 +2243,6 @@ mod tests {
|
||||
selected: p.id,
|
||||
navigation_focus: None,
|
||||
selected_bend: None,
|
||||
selected_attachment: None,
|
||||
zoom,
|
||||
pan: Vector::new(20., 30.),
|
||||
editable: true,
|
||||
@@ -2529,8 +2380,6 @@ mod tests {
|
||||
diagram,
|
||||
route: EdgeRoute {
|
||||
edge,
|
||||
source_attachment: None,
|
||||
target_attachment: None,
|
||||
waypoints: vec![Position { x: 400., y: -100. }],
|
||||
label: None,
|
||||
},
|
||||
@@ -2573,7 +2422,6 @@ mod tests {
|
||||
epoch: 0,
|
||||
},
|
||||
selected_bend: None,
|
||||
selected_attachment: None,
|
||||
editable: true,
|
||||
placing: false,
|
||||
connection: None,
|
||||
@@ -2651,7 +2499,6 @@ mod tests {
|
||||
epoch: 0,
|
||||
},
|
||||
selected_bend: None,
|
||||
selected_attachment: None,
|
||||
editable: true,
|
||||
placing: false,
|
||||
connection: None,
|
||||
@@ -2750,7 +2597,6 @@ mod tests {
|
||||
epoch: 0,
|
||||
},
|
||||
selected_bend: None,
|
||||
selected_attachment: None,
|
||||
editable: true,
|
||||
placing: false,
|
||||
connection: None,
|
||||
@@ -2880,7 +2726,6 @@ mod tests {
|
||||
epoch: 0,
|
||||
},
|
||||
selected_bend: None,
|
||||
selected_attachment: None,
|
||||
editable: true,
|
||||
placing: false,
|
||||
connection: None,
|
||||
@@ -2936,7 +2781,6 @@ mod tests {
|
||||
epoch: 0,
|
||||
},
|
||||
selected_bend: None,
|
||||
selected_attachment: None,
|
||||
editable: true,
|
||||
placing: false,
|
||||
connection: None,
|
||||
@@ -3012,7 +2856,6 @@ mod tests {
|
||||
epoch: 0,
|
||||
},
|
||||
selected_bend: None,
|
||||
selected_attachment: None,
|
||||
editable: true,
|
||||
placing: false,
|
||||
connection: None,
|
||||
@@ -3047,7 +2890,6 @@ mod tests {
|
||||
epoch: 0,
|
||||
},
|
||||
selected_bend: None,
|
||||
selected_attachment: None,
|
||||
editable: true,
|
||||
placing: false,
|
||||
connection: None,
|
||||
|
||||
@@ -1,446 +0,0 @@
|
||||
use super::*;
|
||||
use crate::app::{App, Page};
|
||||
use iced::widget::canvas::Program;
|
||||
|
||||
fn fixture() -> App {
|
||||
let mut p = Project::empty();
|
||||
let a = p.add(Kind::Activity, "Send", 100., 150.);
|
||||
let b = p.add(Kind::Activity, "Receive", 650., 150.);
|
||||
let edge = p.relate(RelationKind::Interaction, "Report", a, b);
|
||||
let diagram = p.ensure_diagram(Layer::Oa);
|
||||
p.diagrams
|
||||
.get_mut(&diagram)
|
||||
.unwrap()
|
||||
.routes
|
||||
.push(EdgeRoute {
|
||||
edge,
|
||||
waypoints: vec![Position { x: 450., y: 350. }],
|
||||
label: Some(EdgeLabel {
|
||||
fraction: 0.4,
|
||||
offset: Position { x: 5., y: 10. },
|
||||
}),
|
||||
source_attachment: None,
|
||||
target_attachment: Some(EdgeAttachment {
|
||||
side: PortAnchorSide::Left,
|
||||
offset: 0.6,
|
||||
}),
|
||||
});
|
||||
let mut app = App::with_project(p);
|
||||
app.page = Page::Diagram;
|
||||
app.active_diagram = Some(diagram);
|
||||
let _ = app.update(Message::Select(edge));
|
||||
app
|
||||
}
|
||||
|
||||
fn canvas(app: &App, zoom: f32) -> DiagramCanvas<'_> {
|
||||
DiagramCanvas {
|
||||
allocation: None,
|
||||
project: app.project(),
|
||||
view_index: None,
|
||||
diagram: app.active_diagram.unwrap(),
|
||||
selected: app.selected,
|
||||
navigation_focus: None,
|
||||
zoom,
|
||||
pan: Vector::new(40., 50.),
|
||||
context: app.route_context().unwrap(),
|
||||
selected_bend: None,
|
||||
selected_attachment: app
|
||||
.selected_attachment
|
||||
.filter(|(c, e, _)| Some(*c) == app.route_context() && *e == app.selected)
|
||||
.map(|(_, _, end)| end),
|
||||
editable: !app.draft_dirty() && !app.source_dirty(),
|
||||
placing: false,
|
||||
connection: None,
|
||||
}
|
||||
}
|
||||
fn bounds() -> Rectangle {
|
||||
Rectangle::new(Point::new(120., 80.), Size::new(2400., 1800.))
|
||||
}
|
||||
fn screen(c: &DiagramCanvas<'_>, p: Position) -> Point {
|
||||
Point::new(
|
||||
bounds().x + p.x * c.zoom + c.pan.x,
|
||||
bounds().y + p.y * c.zoom + c.pan.y,
|
||||
)
|
||||
}
|
||||
fn send(
|
||||
c: &DiagramCanvas<'_>,
|
||||
state: &mut State,
|
||||
event: mouse::Event,
|
||||
final_cursor: Point,
|
||||
) -> Option<Message> {
|
||||
c.update(
|
||||
state,
|
||||
&canvas::Event::Mouse(event),
|
||||
bounds(),
|
||||
mouse::Cursor::Available(final_cursor),
|
||||
)
|
||||
.and_then(|a| a.into_inner().0)
|
||||
}
|
||||
fn positions(c: &DiagramCanvas<'_>) -> (Point, Point) {
|
||||
let scene = c.scene(&State::default());
|
||||
let start = *scene.routed.edges[&c.selected].points.first().unwrap();
|
||||
let node = route_edit::attachment_node(
|
||||
c.project,
|
||||
&c.project.diagrams[&c.diagram],
|
||||
&scene.view,
|
||||
c.selected,
|
||||
EdgeEnd::Source,
|
||||
)
|
||||
.unwrap();
|
||||
let end = oa_core::routing::attachment_point(
|
||||
c.project,
|
||||
&c.project.diagrams[&c.diagram],
|
||||
node,
|
||||
&EdgeAttachment {
|
||||
side: PortAnchorSide::Top,
|
||||
offset: 0.25,
|
||||
},
|
||||
);
|
||||
(screen(c, start), screen(c, end))
|
||||
}
|
||||
fn drag(app: &App, zoom: f32) -> Message {
|
||||
let c = canvas(app, zoom);
|
||||
let (start, end) = positions(&c);
|
||||
let mut state = State::default();
|
||||
let messages = [
|
||||
mouse::Event::CursorMoved { position: start },
|
||||
mouse::Event::ButtonPressed(mouse::Button::Left),
|
||||
mouse::Event::CursorMoved { position: end },
|
||||
mouse::Event::ButtonReleased(mouse::Button::Left),
|
||||
]
|
||||
.into_iter()
|
||||
.filter_map(|e| send(&c, &mut state, e, end))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(messages.len(), 1);
|
||||
messages.into_iter().next().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attachment_event_batches_commit_one_source_history_change_at_three_zooms() {
|
||||
for zoom in [0.4, 1., 2.] {
|
||||
let mut app = fixture();
|
||||
let before = app.document.bundle().clone();
|
||||
let project = app.project().clone();
|
||||
let revision = app.document.revision();
|
||||
let message = drag(&app, zoom);
|
||||
let Message::Route(RouteEvent::CommitAttachment(_, ref route, EdgeEnd::Source, _)) =
|
||||
message
|
||||
else {
|
||||
panic!("attachment commit required")
|
||||
};
|
||||
assert_eq!(
|
||||
route.source_attachment,
|
||||
Some(EdgeAttachment {
|
||||
side: PortAnchorSide::Top,
|
||||
offset: 0.25
|
||||
})
|
||||
);
|
||||
let intended = route.clone();
|
||||
let _ = app.update(message);
|
||||
assert_eq!(app.document.revision(), revision + 1);
|
||||
let mut expected = project;
|
||||
route_edit::with_route(
|
||||
expected
|
||||
.diagrams
|
||||
.get_mut(&app.active_diagram.unwrap())
|
||||
.unwrap(),
|
||||
intended,
|
||||
);
|
||||
assert_eq!(
|
||||
*app.project(),
|
||||
expected,
|
||||
"only the intended view attachment changes"
|
||||
);
|
||||
let after = app.document.bundle().clone();
|
||||
assert_eq!(
|
||||
oa_core::source_project::compile_bundle(&after, Default::default())
|
||||
.unwrap()
|
||||
.decoded
|
||||
.project,
|
||||
expected
|
||||
);
|
||||
let _ = app.update(Message::Undo);
|
||||
assert_eq!(app.document.bundle(), &before);
|
||||
let _ = app.update(Message::Redo);
|
||||
assert_eq!(app.document.bundle(), &after);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attachment_stationary_return_and_escape_leave_source_unchanged() {
|
||||
let mut app = fixture();
|
||||
let before = app.document.bundle().clone();
|
||||
let c = canvas(&app, 1.);
|
||||
let (start, end) = positions(&c);
|
||||
let mut state = State::default();
|
||||
for event in [
|
||||
mouse::Event::CursorMoved { position: start },
|
||||
mouse::Event::ButtonPressed(mouse::Button::Left),
|
||||
mouse::Event::CursorMoved { position: end },
|
||||
mouse::Event::CursorMoved { position: start },
|
||||
mouse::Event::ButtonReleased(mouse::Button::Left),
|
||||
] {
|
||||
assert!(send(&c, &mut state, event, start).is_none());
|
||||
}
|
||||
for event in [
|
||||
mouse::Event::CursorMoved { position: start },
|
||||
mouse::Event::ButtonPressed(mouse::Button::Left),
|
||||
mouse::Event::CursorMoved { position: end },
|
||||
] {
|
||||
assert!(send(&c, &mut state, event, end).is_none());
|
||||
}
|
||||
let _ = app.update(Message::Escape);
|
||||
assert!(
|
||||
send(
|
||||
&canvas(&app, 1.),
|
||||
&mut state,
|
||||
mouse::Event::ButtonReleased(mouse::Button::Left),
|
||||
end
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
assert_eq!(app.document.bundle(), &before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attachment_camera_layout_focus_and_outside_release_cancel_after_motion() {
|
||||
for cancel in 0..8 {
|
||||
let app = fixture();
|
||||
let before = app.document.bundle().clone();
|
||||
let mut c = canvas(&app, 1.);
|
||||
let (start, end) = positions(&c);
|
||||
let mut state = State::default();
|
||||
for event in [
|
||||
mouse::Event::CursorMoved { position: start },
|
||||
mouse::Event::ButtonPressed(mouse::Button::Left),
|
||||
mouse::Event::CursorMoved { position: end },
|
||||
] {
|
||||
assert!(send(&c, &mut state, event, end).is_none());
|
||||
}
|
||||
assert!(state.route_drag.as_ref().is_some_and(|g| g.changed));
|
||||
let mut current_bounds = bounds();
|
||||
let mut release = end;
|
||||
match cancel {
|
||||
0 => c.zoom = 2.,
|
||||
1 => c.pan.x += 20.,
|
||||
2 => current_bounds.x += 20.,
|
||||
3 => current_bounds.height -= 100.,
|
||||
4..=6 => {
|
||||
let event = match cancel {
|
||||
4 => canvas::Event::Window(iced::window::Event::Unfocused),
|
||||
5 => canvas::Event::Window(iced::window::Event::Resized(Size::new(800., 600.))),
|
||||
_ => canvas::Event::Mouse(mouse::Event::CursorLeft),
|
||||
};
|
||||
let message = c
|
||||
.update(
|
||||
&mut state,
|
||||
&event,
|
||||
current_bounds,
|
||||
mouse::Cursor::Available(end),
|
||||
)
|
||||
.and_then(|a| a.into_inner().0);
|
||||
assert!(message.is_none());
|
||||
}
|
||||
_ => {
|
||||
release = Point::new(-100., -100.);
|
||||
assert!(
|
||||
send(
|
||||
&c,
|
||||
&mut state,
|
||||
mouse::Event::CursorMoved { position: release },
|
||||
release
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
}
|
||||
let message = c
|
||||
.update(
|
||||
&mut state,
|
||||
&canvas::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)),
|
||||
current_bounds,
|
||||
mouse::Cursor::Available(release),
|
||||
)
|
||||
.and_then(|a| a.into_inner().0);
|
||||
assert!(message.is_none(), "cancellation case {cancel}");
|
||||
assert!(state.route_drag.is_none(), "cancellation case {cancel}");
|
||||
assert_eq!(app.document.bundle(), &before);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attachment_wheel_and_middle_pan_cancel_before_same_batch_left_release() {
|
||||
for wheel in [true, false] {
|
||||
let mut app = fixture();
|
||||
let before = app.document.bundle().clone();
|
||||
let revision = app.document.revision();
|
||||
let c = canvas(&app, 1.);
|
||||
let (start, end) = positions(&c);
|
||||
let mut state = State::default();
|
||||
for event in [
|
||||
mouse::Event::CursorMoved { position: start },
|
||||
mouse::Event::ButtonPressed(mouse::Button::Left),
|
||||
mouse::Event::CursorMoved { position: end },
|
||||
] {
|
||||
assert!(send(&c, &mut state, event, end).is_none());
|
||||
}
|
||||
let camera_message = send(
|
||||
&c,
|
||||
&mut state,
|
||||
if wheel {
|
||||
mouse::Event::WheelScrolled {
|
||||
delta: mouse::ScrollDelta::Lines { x: 0., y: 1. },
|
||||
}
|
||||
} else {
|
||||
mouse::Event::ButtonPressed(mouse::Button::Middle)
|
||||
},
|
||||
end,
|
||||
);
|
||||
assert_eq!(camera_message.is_some(), wheel);
|
||||
assert!(state.route_drag.is_none());
|
||||
// Deliberately retain the same canvas and defer App camera dispatch.
|
||||
assert!(
|
||||
send(
|
||||
&c,
|
||||
&mut state,
|
||||
mouse::Event::ButtonReleased(mouse::Button::Left),
|
||||
end
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
if !wheel {
|
||||
assert!(
|
||||
send(
|
||||
&c,
|
||||
&mut state,
|
||||
mouse::Event::ButtonReleased(mouse::Button::Middle),
|
||||
end
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
if let Some(message) = camera_message {
|
||||
assert!(matches!(message, Message::Zoom(_)));
|
||||
let _ = app.update(message);
|
||||
}
|
||||
assert_eq!(app.document.revision(), revision);
|
||||
assert_eq!(app.document.bundle(), &before);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attachment_fast_release_uses_release_position_and_stays_in_one_view() {
|
||||
let mut app = fixture();
|
||||
let original = app.active_diagram.unwrap();
|
||||
let _ = app.update(Message::DuplicateDiagram(original));
|
||||
let other = app.active_diagram.unwrap();
|
||||
assert_ne!(original, other);
|
||||
app.active_diagram = Some(original);
|
||||
let edge = app.project().diagrams[&original].routes[0].edge;
|
||||
let _ = app.update(Message::Select(edge));
|
||||
let untouched = app.project().diagrams[&other].clone();
|
||||
let c = canvas(&app, 0.7);
|
||||
let (start, end) = positions(&c);
|
||||
let mut state = State::default();
|
||||
assert!(
|
||||
send(
|
||||
&c,
|
||||
&mut state,
|
||||
mouse::Event::ButtonPressed(mouse::Button::Left),
|
||||
start
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
let message = send(
|
||||
&c,
|
||||
&mut state,
|
||||
mouse::Event::ButtonReleased(mouse::Button::Left),
|
||||
end,
|
||||
)
|
||||
.expect("release completes drag without final move");
|
||||
let _ = app.update(message);
|
||||
assert_eq!(
|
||||
app.project().diagrams[&original].routes[0].source_attachment,
|
||||
Some(EdgeAttachment {
|
||||
side: PortAnchorSide::Top,
|
||||
offset: 0.25
|
||||
})
|
||||
);
|
||||
assert_eq!(app.project().diagrams[&other], untouched);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attachment_stale_context_wrong_placement_and_unowned_route_fields_are_rejected() {
|
||||
let mut app = fixture();
|
||||
let before = app.document.bundle().clone();
|
||||
let message = drag(&app, 1.);
|
||||
let Message::Route(RouteEvent::CommitAttachment(context, route, end, node)) = message else {
|
||||
panic!()
|
||||
};
|
||||
let mut changed = route.clone();
|
||||
changed.waypoints.clear();
|
||||
let _ = app.update(Message::Route(RouteEvent::CommitAttachment(
|
||||
context, changed, end, node,
|
||||
)));
|
||||
let _ = app.update(Message::Route(RouteEvent::CommitAttachment(
|
||||
context,
|
||||
route.clone(),
|
||||
end,
|
||||
Id::new_v4(),
|
||||
)));
|
||||
let _ = app.update(Message::Escape);
|
||||
let _ = app.update(Message::Route(RouteEvent::CommitAttachment(
|
||||
context, route, end, node,
|
||||
)));
|
||||
assert_eq!(app.document.bundle(), &before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attachment_reset_preserves_opposite_end_pins_label_and_semantics() {
|
||||
let mut app = fixture();
|
||||
let before = app.project().clone();
|
||||
let _ = app.update(Message::Route(RouteEvent::ResetAttachment(EdgeEnd::Target)));
|
||||
let mut expected = before;
|
||||
expected
|
||||
.diagrams
|
||||
.get_mut(&app.active_diagram.unwrap())
|
||||
.unwrap()
|
||||
.routes[0]
|
||||
.target_attachment = None;
|
||||
assert_eq!(*app.project(), expected);
|
||||
let revision = app.document.revision();
|
||||
let _ = app.update(Message::Route(RouteEvent::ResetAttachment(EdgeEnd::Target)));
|
||||
assert_eq!(app.document.revision(), revision);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coincident_attachment_handles_honor_explicit_end_choice() {
|
||||
let mut app = fixture();
|
||||
let _ = app.update(Message::Route(RouteEvent::ChooseAttachment(
|
||||
EdgeEnd::Target,
|
||||
)));
|
||||
let c = canvas(&app, 1.);
|
||||
let mut scene = Scene {
|
||||
work: None,
|
||||
view: projection::project_view(c.project, c.diagram),
|
||||
routed: Arc::new(route_diagram(
|
||||
c.project,
|
||||
&c.project.diagrams[&c.diagram],
|
||||
&projection::project_view(c.project, c.diagram),
|
||||
)),
|
||||
};
|
||||
let routed = Arc::make_mut(&mut scene.routed);
|
||||
let edge = routed.edges.get_mut(&c.selected).unwrap();
|
||||
let start = *edge.points.first().unwrap();
|
||||
*edge.points.last_mut().unwrap() = start;
|
||||
assert_eq!(
|
||||
c.attachment_at(&scene, point(start)).unwrap().0,
|
||||
EdgeEnd::Target
|
||||
);
|
||||
let mut source = c;
|
||||
source.selected_attachment = Some(EdgeEnd::Source);
|
||||
assert_eq!(
|
||||
source.attachment_at(&scene, point(start)).unwrap().0,
|
||||
EdgeEnd::Source
|
||||
);
|
||||
}
|
||||
@@ -78,11 +78,7 @@ impl<Message, R: renderer::Renderer> Widget<Message, Theme, R> for Owner<'_, Mes
|
||||
}
|
||||
if state.context.is_none_or(|previous| {
|
||||
(previous.document, previous.diagram, previous.epoch)
|
||||
!= (
|
||||
self.context.document,
|
||||
self.context.diagram,
|
||||
self.context.epoch,
|
||||
)
|
||||
!= (self.context.document, self.context.diagram, self.context.epoch)
|
||||
}) || !self.enabled
|
||||
{
|
||||
state.focused = false;
|
||||
|
||||
@@ -62,7 +62,6 @@ fn canvas(app: &App) -> DiagramCanvas<'_> {
|
||||
pan: app.pan,
|
||||
context: app.route_context().unwrap(),
|
||||
selected_bend: None,
|
||||
selected_attachment: None,
|
||||
editable: true,
|
||||
placing: false,
|
||||
connection: app.connection.as_ref(),
|
||||
|
||||
@@ -70,7 +70,6 @@ fn canvas(app: &App, zoom: f32) -> DiagramCanvas<'_> {
|
||||
pan: app.pan,
|
||||
context: app.route_context().unwrap(),
|
||||
selected_bend: None,
|
||||
selected_attachment: None,
|
||||
editable: true,
|
||||
placing: false,
|
||||
connection: app.connection.as_ref(),
|
||||
|
||||
@@ -16,7 +16,6 @@ fn canvas(project: &Project, diagram: Id, zoom: f32) -> DiagramCanvas<'_> {
|
||||
epoch: 0,
|
||||
},
|
||||
selected_bend: None,
|
||||
selected_attachment: None,
|
||||
editable: true,
|
||||
placing: false,
|
||||
connection: None,
|
||||
|
||||
@@ -186,7 +186,6 @@ mod tests {
|
||||
pan: Vector::ZERO,
|
||||
context: app.route_context().unwrap(),
|
||||
selected_bend: None,
|
||||
selected_attachment: None,
|
||||
editable: true,
|
||||
placing: false,
|
||||
connection: None,
|
||||
|
||||
@@ -307,7 +307,7 @@ fn article(topic: Topic) -> Article {
|
||||
"Choose and edit the relationship you mean",
|
||||
&[
|
||||
"Use the diagram selector or + View to work with a focused diagram. View properties edits its name, perspective, projection and Saved view visibility. Apply changes commits the view settings.",
|
||||
"Choose Containment for semantic children, Architecture · allocated work for responsibilities, Functional dataflow for work and exchanges, or Missions & capabilities for outcomes. All relationships explores one layer; OA ↔ SA traceability can show placed elements from both layers and their correspondence lines alongside ordinary relationships.",
|
||||
"Choose Containment for semantic children, Architecture · allocated work for responsibilities, Functional dataflow for work and exchanges, or Missions & capabilities for outcomes. All relationships is a broader exploration view; OA ↔ SA traceability displays counterparts.",
|
||||
"Select an element and edit Semantic parent to change ownership. Use an Allocation relationship to change responsibility. Apply changes validates the semantic change.",
|
||||
"Scroll to zoom and use Fit to frame the current view. In allocated-work views, dragging work changes its assignment and placement together. Choose Arrange next drag or press Alt+A to move work without changing responsibility for that one drag; Escape cancels the armed action. Auto-layout arranges the whole view. Export SVG produces a shareable view.",
|
||||
"Select a diagram node to access Placement, Fold / unfold and Hide. With the canvas focused, Delete hides its placement; Shift+Delete asks to remove the model element. Other views retain a hidden element. Undo restores either change.",
|
||||
@@ -317,16 +317,6 @@ fn article(topic: Topic) -> Article {
|
||||
"Keep diagrams readable",
|
||||
"Use separate views for responsibility, decomposition and cross-layer coverage. Place existing elements with Show in this diagram. A placement's Diagram field can relocate it while preserving its identity and artifact references. Do not duplicate a semantic element merely to show it in another view.",
|
||||
),
|
||||
walkthrough(
|
||||
"Show a correspondence between OA and SA",
|
||||
&[
|
||||
"Choose OA ↔ SA traceability in View content. Changing this setting does not create counterparts or add their placements, so an existing single-layer diagram can initially look unchanged.",
|
||||
"Use Review layer traces to inspect named Operational and System endpoints and the meaning of their link. This opens the model's layer-trace list with no search restriction; traces between relationships are reviewed here rather than drawn between element cards.",
|
||||
"In the chosen architecture view, use Add existing elements. The chooser offers compatible elements from both layers. Select an existing endpoint and click its position in the canvas; repeat for the other end if needed. This adds a placement, not a new semantic element or trace. For port correspondences, place the ports' owners.",
|
||||
"The view shows correspondence lines alongside ordinary relationships. The shown-line count reflects the current projection and visibility settings, not trace coverage. If a line is hidden, use View visibility and inspect Saved view visibility; placing an endpoint does not remove explicit hides or filters.",
|
||||
"If the counterpart is already placed, use Include already shown (Locate) in the chooser. Undo restores an added placement; saving preserves each view's placements independently. Use the Traceability page's existing counterpart or manual-trace workflow when the semantic link itself does not yet exist.",
|
||||
],
|
||||
),
|
||||
walkthrough(
|
||||
"Move work between performers",
|
||||
&[
|
||||
@@ -354,7 +344,7 @@ fn article(topic: Topic) -> Article {
|
||||
&[
|
||||
"Select an architecture connection and drag a segment to add a bend. Right-click a line for Add bend here; right-click a circle for Remove bend. With the canvas focused, Insert adds a bend and Delete removes the selected bend. Drag the label independently. Escape cancels a gesture; Undo reverses one completed drag. Shift+F10 opens the selected diagram item's context menu; use arrow keys and Enter to choose an action.",
|
||||
"Automatic orthogonal routing avoids cards and container headers where space permits. Amber connections need attention: move an obstructing element or a bend. In Select / move, drag a port to any border of its owner. Manual positions are per diagram; right-click Reset automatic position restores connection-aware placement. Ports remain attached to their actual owners. Manual routes are saved separately for each diagram and appear in source, API and SVG export.",
|
||||
"Select a connection and drag a square endpoint handle along its element boundary to move its attachment in this view without changing what it connects. If handles overlap, right-click the selected connection or use its Actions menu and choose Move source attachment or Move target attachment, then drag the handle. Connections to semantic ports retain their port attachment; move the port instead. Reset source attachment or Reset target attachment restores that end's automatic placement while preserving the other end, pins and label. Reset routing clears all these route overrides in this view. Auto-layout preserves pins. Moving one endpoint keeps pins fixed; moving a container carrying both endpoints translates their pins. When folding promotes an endpoint, expand it again to resume and edit its saved route.",
|
||||
"Right-click a connection and choose Reset routing to clear its pins and label position in this view. The Properties Actions menu provides the same commands. Auto-layout preserves pins. Moving one endpoint keeps pins fixed; moving a container carrying both endpoints translates their pins. When folding promotes an endpoint, expand it again to resume and edit its saved route.",
|
||||
],
|
||||
),
|
||||
explain(
|
||||
@@ -574,8 +564,7 @@ fn article(topic: Topic) -> Article {
|
||||
"Optionally select Create the first state view, then Create and open. The new machine starts without regions or states. For an existing machine, Create diagram places its existing regions and vertices in a saved view; + View creates another independent layout.",
|
||||
"Choose + Region and click the highlighted machine, or a State/Mode for a nested region. Choose Initial, State, Mode or another kind from + State / symbol, then click its highlighted owning region. Rename the new element in Properties.",
|
||||
"Choose Transition and drag from a source vertex to a compatible target, or click the source and then the target. In Select / move, drag states or containers to arrange them; a container moves its descendant placements without changing ownership.",
|
||||
"Select a drawn transition and drag its endpoint handle to reconnect it. Its right-click menu also offers Reconnect source and Reconnect target; choose one, then click a compatible vertex. To change only where a line touches its existing vertex, choose Adjust source attachment or Adjust target attachment, then drag the highlighted tip along the boundary. Reset source attachment or Reset target attachment restores automatic placement for that end without changing the transition's meaning or other route settings. Escape cancels an attachment drag.",
|
||||
"To shape a selected transition, drag an exposed line segment to add a bend, or drag a square bend handle to move it. Right-click a square and choose Remove bend. The transition's Actions menu also offers Add bend and Clear bends; clearing bends keeps its attachment points and label placement. Bends belong to this saved view. A click alone changes nothing, Escape cancels a drag, and Undo restores the complete edit. Internal transition rows and folded connections do not expose bend editing while their route is hidden.",
|
||||
"Select a drawn transition and drag its endpoint handle to reconnect it. Its right-click menu also offers Reconnect source and Reconnect target; choose one, then click a compatible vertex.",
|
||||
"Right-click a state or the background for New transition with details… to create Local, Internal or timed behavior. Right-click an existing transition and choose Edit transition to edit its kind, triggers, guard and effects. EntryPoint outgoing transitions use Local. Apply accepts the complete form; invalid drafts remain available for correction.",
|
||||
],
|
||||
),
|
||||
|
||||
@@ -28,6 +28,7 @@ mod recovery;
|
||||
mod route_edit;
|
||||
mod semantic_review;
|
||||
mod source_annotations;
|
||||
mod source_indentation;
|
||||
mod source_navigation;
|
||||
mod source_rename;
|
||||
mod source_tree;
|
||||
|
||||
@@ -14,7 +14,6 @@ pub struct Request {
|
||||
pub placement: Option<Id>,
|
||||
pub bend: Option<usize>,
|
||||
pub route_point: Option<Position>,
|
||||
pub route_segment: Option<(usize, Position)>,
|
||||
pub creation_point: Option<Position>,
|
||||
pub canvas: Option<route_edit::Context>,
|
||||
pub source_file: Option<String>,
|
||||
@@ -27,7 +26,6 @@ impl Request {
|
||||
placement: None,
|
||||
bend: None,
|
||||
route_point: None,
|
||||
route_segment: None,
|
||||
creation_point: None,
|
||||
canvas: None,
|
||||
source_file: None,
|
||||
@@ -242,33 +240,6 @@ pub fn popup(app: &App) -> Option<Menu<Message>> {
|
||||
.is_none_or(|(s, t)| s == e.source && t == e.target)
|
||||
});
|
||||
let route = diagram.and_then(|d| d.routes.iter().find(|r| r.edge == id));
|
||||
if let Some((diagram, view)) = diagram.zip(view.as_ref()) {
|
||||
for (end, choose, reset) in [
|
||||
(
|
||||
EdgeEnd::Source,
|
||||
"Move source attachment",
|
||||
"Reset source attachment",
|
||||
),
|
||||
(
|
||||
EdgeEnd::Target,
|
||||
"Move target attachment",
|
||||
"Reset target attachment",
|
||||
),
|
||||
] {
|
||||
let eligible = editable
|
||||
&& route_edit::attachment_node(p, diagram, view, id, end).is_some();
|
||||
entries.push(item(
|
||||
choose,
|
||||
Message::Route(route_edit::Event::ChooseAttachment(end)),
|
||||
eligible,
|
||||
));
|
||||
entries.push(item(
|
||||
reset,
|
||||
Message::Route(route_edit::Event::ResetAttachment(end)),
|
||||
eligible && route.is_some_and(|route| route.attachment(end).is_some()),
|
||||
));
|
||||
}
|
||||
}
|
||||
if let Some(point) = request.route_point {
|
||||
entries.push(
|
||||
item(
|
||||
@@ -458,8 +429,7 @@ pub fn popup(app: &App) -> Option<Menu<Message>> {
|
||||
entries.push(item(
|
||||
"Show in this diagram",
|
||||
Message::PlaceSelected,
|
||||
(d.layer == e.layer() || d.projection == ProjectionKind::Traceability)
|
||||
&& d.projection.includes(e.kind()),
|
||||
d.layer == e.layer() && d.projection.includes(e.kind()),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -590,7 +560,6 @@ fn relation_actions(kind: Kind) -> &'static [(&'static str, RelationKind, bool)]
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
include!("model_menu_traceability_tests.rs");
|
||||
fn send(app: &mut App, message: Message) {
|
||||
let _ = app.update(message);
|
||||
}
|
||||
@@ -931,36 +900,6 @@ mod tests {
|
||||
assert_eq!(app.document.bundle(), &changed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attachment_end_choice_is_captured_without_mutating_source_and_stale_menu_is_rejected() {
|
||||
let mut project = Project::empty();
|
||||
let source = project.add(Kind::Activity, "Send", 0., 0.);
|
||||
let target = project.add(Kind::Activity, "Receive", 650., 0.);
|
||||
let edge = project.relate(RelationKind::Interaction, "Report", source, target);
|
||||
let mut app = App::with_project(project);
|
||||
let before = app.document.bundle().clone();
|
||||
let mut request = Request::object(edge, Point::ORIGIN);
|
||||
request.canvas = app.route_context();
|
||||
send(&mut app, Message::Menu(Event::Open(request.clone())));
|
||||
let action = choose(&app, "Move target attachment");
|
||||
send(&mut app, action);
|
||||
assert_eq!(
|
||||
app.selected_attachment,
|
||||
Some((app.route_context().unwrap(), edge, EdgeEnd::Target))
|
||||
);
|
||||
assert_eq!(app.document.bundle(), &before);
|
||||
request.canvas = app.route_context();
|
||||
send(&mut app, Message::Menu(Event::Open(request)));
|
||||
let stale = choose(&app, "Move source attachment");
|
||||
send(&mut app, Message::Escape);
|
||||
send(&mut app, stale);
|
||||
assert!(
|
||||
app.selected_attachment
|
||||
.is_none_or(|(context, _, _)| Some(context) != app.route_context())
|
||||
);
|
||||
assert_eq!(app.document.bundle(), &before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn show_from_explorer_places_unfolds_and_keeps_semantic_selection() {
|
||||
let mut p = Project::empty();
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
#[test]
|
||||
fn traceability_show_menu_matches_cross_layer_and_port_placement_eligibility() {
|
||||
let mut project = Project::empty();
|
||||
let sa = project
|
||||
.add_element(Kind::Function, Layer::Sa, "Counterpart", None)
|
||||
.unwrap();
|
||||
let port = Id::new_v4();
|
||||
project.elements.insert(
|
||||
port,
|
||||
Element {
|
||||
id: port,
|
||||
alias: "legacy_port".into(),
|
||||
name: "Port".into(),
|
||||
description: String::new(),
|
||||
parent: Some(sa),
|
||||
body: ElementBody::shared_in(
|
||||
Layer::Sa,
|
||||
SharedBody::Port {
|
||||
contract: PortContract::Legacy,
|
||||
legacy: Some(LegacyPortContract {
|
||||
direction: PortDirection::InOut,
|
||||
interface: None,
|
||||
}),
|
||||
},
|
||||
),
|
||||
},
|
||||
);
|
||||
let diagram = project.ensure_diagram(Layer::Oa);
|
||||
oa_core::validate_candidate(&project).unwrap();
|
||||
for projection in [ProjectionKind::Overview, ProjectionKind::Traceability] {
|
||||
let mut p = project.clone();
|
||||
p.diagrams.get_mut(&diagram).unwrap().projection = projection;
|
||||
let mut app = App::with_project(p);
|
||||
app.page = Page::Diagram;
|
||||
app.layer = Layer::Oa;
|
||||
app.active_diagram = Some(diagram);
|
||||
for object in [sa, port] {
|
||||
send(&mut app, open(object, Point::ORIGIN));
|
||||
let action = popup(&app)
|
||||
.unwrap()
|
||||
.entries
|
||||
.into_iter()
|
||||
.find_map(|e| match e {
|
||||
Entry::Action { label, message, .. } if label == "Show in this diagram" => {
|
||||
Some(message)
|
||||
}
|
||||
_ => None,
|
||||
});
|
||||
if object == port {
|
||||
assert!(
|
||||
action.is_none(),
|
||||
"Ports never acquire independent node placements"
|
||||
);
|
||||
} else {
|
||||
assert_eq!(
|
||||
action.expect("non-port placement entry").is_some(),
|
||||
projection == ProjectionKind::Traceability
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,9 +14,6 @@ pub struct Context {
|
||||
pub enum Event {
|
||||
SelectBend(Id, usize),
|
||||
Commit(Context, EdgeRoute, Option<usize>),
|
||||
CommitAttachment(Context, EdgeRoute, EdgeEnd, Id),
|
||||
ChooseAttachment(EdgeEnd),
|
||||
ResetAttachment(EdgeEnd),
|
||||
AddBend,
|
||||
AddBendAt(oa_core::Position),
|
||||
RemoveBend,
|
||||
@@ -26,14 +23,7 @@ pub enum Event {
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Handle {
|
||||
Bend(usize),
|
||||
Label {
|
||||
original: Position,
|
||||
},
|
||||
Attachment {
|
||||
end: EdgeEnd,
|
||||
owner: projection::ProjectedNode,
|
||||
original: Option<EdgeAttachment>,
|
||||
},
|
||||
Label { original: Position },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -68,57 +58,8 @@ impl Gesture {
|
||||
};
|
||||
}
|
||||
}
|
||||
Handle::Attachment { .. } => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn move_attachment_to(&mut self, point: Point, zoom: f32) {
|
||||
let Handle::Attachment {
|
||||
end,
|
||||
ref owner,
|
||||
original,
|
||||
} = self.handle
|
||||
else {
|
||||
self.move_to(point, zoom);
|
||||
return;
|
||||
};
|
||||
if point.distance(self.start) * zoom < 3. {
|
||||
self.route.set_attachment(end, original);
|
||||
self.changed = false;
|
||||
return;
|
||||
}
|
||||
self.changed = true;
|
||||
let attachment = attachment_from_point(
|
||||
owner,
|
||||
position(point),
|
||||
self.route.attachment(end).map(|a| a.side),
|
||||
6. / zoom,
|
||||
);
|
||||
self.route.set_attachment(end, Some(attachment));
|
||||
}
|
||||
}
|
||||
|
||||
/// The painted endpoint must still be the actual unported placement, not a folded proxy.
|
||||
pub fn attachment_node<'a>(
|
||||
project: &Project,
|
||||
diagram: &Diagram,
|
||||
view: &'a projection::ProjectedView,
|
||||
edge: Id,
|
||||
end: EdgeEnd,
|
||||
) -> Option<&'a projection::ProjectedNode> {
|
||||
if semantic_port_endpoint(project, edge, end).is_some() {
|
||||
return None;
|
||||
}
|
||||
let projected = view.edges.iter().find(|e| e.id == edge)?;
|
||||
let actual = projection::edge_endpoint_placements(project, diagram, edge)?;
|
||||
if actual != (projected.source, projected.target) {
|
||||
return None;
|
||||
}
|
||||
let placement = match end {
|
||||
EdgeEnd::Source => projected.source,
|
||||
EdgeEnd::Target => projected.target,
|
||||
};
|
||||
view.nodes.iter().find(|node| node.id == placement)
|
||||
}
|
||||
|
||||
pub fn stored_route(diagram: &Diagram, edge: Id) -> EdgeRoute {
|
||||
@@ -131,40 +72,10 @@ pub fn stored_route(diagram: &Diagram, edge: Id) -> EdgeRoute {
|
||||
edge,
|
||||
waypoints: Vec::new(),
|
||||
label: None,
|
||||
source_attachment: None,
|
||||
target_attachment: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Insert after the pins preceding the clicked segment along the routed path.
|
||||
pub fn insert_pin_on_segment(
|
||||
route: &mut EdgeRoute,
|
||||
geometry: &RoutedEdge,
|
||||
segment: usize,
|
||||
point: Position,
|
||||
) -> Option<usize> {
|
||||
if route.edge != geometry.id
|
||||
|| geometry.custom_suspended
|
||||
|| route.waypoints != geometry.pins
|
||||
|| route.waypoints.len() >= 128
|
||||
|| !point.x.is_finite()
|
||||
|| !point.y.is_finite()
|
||||
|| geometry
|
||||
.points
|
||||
.get(segment..segment.checked_add(2)?)
|
||||
.is_none()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let index = pin_insertion_index(geometry, segment, point);
|
||||
if index > route.waypoints.len() {
|
||||
return None;
|
||||
}
|
||||
route.waypoints.insert(index, point);
|
||||
Some(index)
|
||||
}
|
||||
|
||||
/// Find a segment when an ordinary caller has only a point, not a captured hit.
|
||||
pub fn insert_pin(route: &mut EdgeRoute, geometry: &RoutedEdge, point: Position) -> usize {
|
||||
let segment = geometry
|
||||
.points
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
//! Source indentation is one workspace edit, with physical line endings intact.
|
||||
use iced::{
|
||||
advanced::{graphics, text},
|
||||
widget::text_editor::{Action, Content, Cursor, Edit, Position},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
const INDENT: &str = " ";
|
||||
|
||||
/// Plain Tab inserts one canonical four-space level at a caret, or prefixes
|
||||
/// every selected line. Shift+Tab removes one leading tab or up to four spaces.
|
||||
/// A selection ending at column zero excludes that final line. All patches are
|
||||
/// local to lines, so CRLF, LF and Unicode bytes outside indentation survive.
|
||||
/// The caller records the resulting buffer once in workspace undo/history.
|
||||
pub fn apply<R>(content: &mut Content<R>, outdent: bool)
|
||||
where
|
||||
R: text::Renderer<Editor = graphics::text::Editor>,
|
||||
{
|
||||
let raw = content.cursor();
|
||||
let selected = content.selection().is_some_and(|s| !s.is_empty());
|
||||
if !selected && !outdent {
|
||||
content.perform(Action::Edit(Edit::Paste(Arc::new(INDENT.into()))));
|
||||
return;
|
||||
}
|
||||
// Determine affected lines before normalizing Word/Line mode. Those modes
|
||||
// expand columns within their endpoint lines; the copied LF count reveals
|
||||
// whether a zero-column endpoint includes the next line or only its edge.
|
||||
let anchor = raw.selection.unwrap_or(raw.position);
|
||||
let first = raw.position.line.min(anchor.line);
|
||||
let mut last = raw.position.line.max(anchor.line);
|
||||
if selected {
|
||||
let selected_text = content.selection().unwrap();
|
||||
let line_breaks = selected_text.bytes().filter(|b| *b == b'\n').count();
|
||||
// Effective selection starts on the minimum raw endpoint line.
|
||||
last = first + line_breaks;
|
||||
if last > first && selected_text.ends_with('\n') {
|
||||
last -= 1;
|
||||
}
|
||||
}
|
||||
let patches: Vec<_> = (first..=last)
|
||||
.filter_map(|line| {
|
||||
let removed = if outdent {
|
||||
let text = content.line(line)?;
|
||||
if text.text.starts_with('\t') {
|
||||
1
|
||||
} else {
|
||||
text.text.bytes().take(4).take_while(|b| *b == b' ').count()
|
||||
}
|
||||
} else {
|
||||
0
|
||||
};
|
||||
(!outdent || removed > 0).then_some((line, removed))
|
||||
})
|
||||
.collect();
|
||||
if patches.is_empty() {
|
||||
return;
|
||||
}
|
||||
let cursor = crate::source_navigation::selection::capture_for_edit(content);
|
||||
let replacement = Arc::new(if outdent {
|
||||
String::new()
|
||||
} else {
|
||||
INDENT.into()
|
||||
});
|
||||
// Descending order leaves Iced's earliest changed line at the first patch.
|
||||
for &(line, removed) in patches.iter().rev() {
|
||||
content.move_to(Cursor {
|
||||
position: Position { line, column: 0 },
|
||||
selection: Some(Position {
|
||||
line,
|
||||
column: removed,
|
||||
}),
|
||||
});
|
||||
content.perform(Action::Edit(Edit::Paste(replacement.clone())));
|
||||
}
|
||||
let transform = |mut position: Position| {
|
||||
if let Some((_, removed)) = patches.iter().find(|(line, _)| *line == position.line) {
|
||||
position.column = if outdent {
|
||||
position.column.saturating_sub(*removed)
|
||||
} else {
|
||||
position.column + INDENT.len()
|
||||
};
|
||||
}
|
||||
position
|
||||
};
|
||||
content.move_to(Cursor {
|
||||
position: transform(cursor.position),
|
||||
selection: cursor.selection.map(transform),
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(all(test, debug_assertions))]
|
||||
mod keyboard_tests;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,106 @@
|
||||
//! Real mounted Iced keyboard dispatch, using the production binding function.
|
||||
use crate::{app::Message, source_navigation::SymbolContext, views::source_tests::FontLayout};
|
||||
use iced::advanced::widget::operation::Focusable;
|
||||
use iced::{
|
||||
Element, Font, Rectangle, Size,
|
||||
advanced::{Layout, Shell, layout, text, widget as core_widget},
|
||||
keyboard::{
|
||||
self, Key, Modifiers,
|
||||
key::{Code, Named, Physical},
|
||||
},
|
||||
widget::{
|
||||
self,
|
||||
text_editor::{Content, State},
|
||||
},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn mounted_tab_dispatches_only_for_focused_editable_editor_and_plain_shift_variants() {
|
||||
for focused in [false, true] {
|
||||
for editable in [false, true] {
|
||||
for modifiers in [
|
||||
Modifiers::empty(),
|
||||
Modifiers::SHIFT,
|
||||
Modifiers::CTRL,
|
||||
Modifiers::ALT,
|
||||
Modifiers::LOGO,
|
||||
] {
|
||||
let context = SymbolContext {
|
||||
session: oa_core::Id::new_v4(),
|
||||
path: "project.arcadia".into(),
|
||||
};
|
||||
let content = Content::<FontLayout>::with_text("rain");
|
||||
let mut editor = widget::text_editor(&content)
|
||||
.font(Font::MONOSPACE)
|
||||
.size(14)
|
||||
.padding(16)
|
||||
.height(iced::Fill)
|
||||
.key_binding(|press| crate::views::source_binding(press, &context, None));
|
||||
if editable {
|
||||
editor = editor.on_action(Message::SourceEdit);
|
||||
}
|
||||
let mut view: Element<'_, Message, iced::Theme, FontLayout> = editor.into();
|
||||
let mut tree = core_widget::Tree::new(view.as_widget());
|
||||
let size = Size::new(600., 240.);
|
||||
let node = view.as_widget_mut().layout(
|
||||
&mut tree,
|
||||
&FontLayout,
|
||||
&layout::Limits::new(Size::ZERO, size),
|
||||
);
|
||||
if focused {
|
||||
tree.state
|
||||
.downcast_mut::<State<text::highlighter::PlainText>>()
|
||||
.focus();
|
||||
}
|
||||
let event = iced::Event::Keyboard(keyboard::Event::KeyPressed {
|
||||
key: Key::Named(Named::Tab),
|
||||
modified_key: Key::Named(Named::Tab),
|
||||
physical_key: Physical::Code(Code::Tab),
|
||||
location: keyboard::Location::Standard,
|
||||
modifiers,
|
||||
text: None,
|
||||
repeat: false,
|
||||
});
|
||||
let mut messages = Vec::new();
|
||||
let captured;
|
||||
{
|
||||
let mut shell = Shell::new(&mut messages);
|
||||
view.as_widget_mut().update(
|
||||
&mut tree,
|
||||
&event,
|
||||
Layout::new(&node),
|
||||
iced::mouse::Cursor::Unavailable,
|
||||
&FontLayout,
|
||||
&mut iced::advanced::clipboard::Null,
|
||||
&mut shell,
|
||||
&Rectangle::with_size(size),
|
||||
);
|
||||
captured = shell.is_event_captured();
|
||||
}
|
||||
let plain = modifiers.is_empty() || modifiers == Modifiers::SHIFT;
|
||||
let expected = focused && editable && plain;
|
||||
let indentation: Vec<_> = messages
|
||||
.iter()
|
||||
.filter_map(|message| match message {
|
||||
Message::SourceIndent(emitted, outdent) => Some((emitted, outdent)),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(
|
||||
indentation.len(),
|
||||
usize::from(expected),
|
||||
"focused={focused}, editable={editable}, modifiers={modifiers:?}"
|
||||
);
|
||||
if expected {
|
||||
assert!(captured, "Tab must not escape to global focus traversal");
|
||||
assert_eq!(indentation[0], (&context, &modifiers.shift()));
|
||||
} else if !focused || !editable {
|
||||
assert!(
|
||||
!captured,
|
||||
"non-editing Tab must remain available to focus traversal"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
use super::*;
|
||||
use iced::widget::text_editor::Motion;
|
||||
|
||||
type Buffer = Content<iced::Renderer>;
|
||||
fn at(line: usize, column: usize) -> Position {
|
||||
Position { line, column }
|
||||
}
|
||||
fn range(start: Position, end: Position, reverse: bool) -> Cursor {
|
||||
Cursor {
|
||||
position: if reverse { start } else { end },
|
||||
selection: Some(if reverse { end } else { start }),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn caret_inserts_four_spaces_and_outdent_removes_only_leading_whitespace() {
|
||||
let mut content = Buffer::with_text(" 雨🚀\r\nnext");
|
||||
content.move_to(Cursor {
|
||||
position: at(0, " 雨".len()),
|
||||
selection: None,
|
||||
});
|
||||
apply(&mut content, false);
|
||||
assert_eq!(content.text(), " 雨 🚀\r\nnext");
|
||||
assert_eq!(content.cursor().position, at(0, " 雨 ".len()));
|
||||
apply(&mut content, true);
|
||||
assert_eq!(content.text(), "雨 🚀\r\nnext");
|
||||
assert_eq!(content.cursor().position, at(0, "雨 ".len()));
|
||||
apply(&mut content, true);
|
||||
assert_eq!(content.text(), "雨 🚀\r\nnext");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn directional_blocks_preserve_unicode_endings_and_exclude_final_zero_column() {
|
||||
let source = "雨\r\n🚀\nlast\r\n";
|
||||
for reverse in [false, true] {
|
||||
let mut content = Buffer::with_text(source);
|
||||
let original = range(at(0, 0), at(2, 0), reverse);
|
||||
content.move_to(original);
|
||||
apply(&mut content, false);
|
||||
assert_eq!(content.text(), " 雨\r\n 🚀\nlast\r\n");
|
||||
assert_eq!(content.cursor(), range(at(0, 4), at(2, 0), reverse));
|
||||
apply(&mut content, true);
|
||||
assert_eq!(content.text(), source);
|
||||
assert_eq!(content.cursor(), original);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outdent_handles_tabs_partial_spaces_and_clamps_endpoint_inside_removed_indent() {
|
||||
for reverse in [false, true] {
|
||||
let mut content = Buffer::with_text("\t雨\r\n 🚀\n fin");
|
||||
content.move_to(range(at(0, 0), at(2, 2), reverse));
|
||||
apply(&mut content, true);
|
||||
assert_eq!(content.text(), "雨\r\n🚀\nfin");
|
||||
assert_eq!(content.cursor(), range(at(0, 0), at(2, 0), reverse));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn word_and_line_selections_indent_effective_range_in_both_directions() {
|
||||
for line_mode in [false, true] {
|
||||
for reverse in [false, true] {
|
||||
let mut content = Buffer::with_text("one alpha\r\ntwo beta\nthree gamma");
|
||||
content.move_to(Cursor {
|
||||
position: if reverse { at(2, 8) } else { at(0, 6) },
|
||||
selection: None,
|
||||
});
|
||||
content.perform(if line_mode {
|
||||
Action::SelectLine
|
||||
} else {
|
||||
Action::SelectWord
|
||||
});
|
||||
content.perform(Action::Select(if reverse {
|
||||
Motion::DocumentStart
|
||||
} else {
|
||||
Motion::DocumentEnd
|
||||
}));
|
||||
let selected = content.selection().unwrap();
|
||||
apply(&mut content, false);
|
||||
assert_eq!(
|
||||
content.text(),
|
||||
" one alpha\r\n two beta\n three gamma"
|
||||
);
|
||||
let cursor = content.cursor();
|
||||
assert_eq!(
|
||||
cursor.position.line < cursor.selection.unwrap().line,
|
||||
reverse
|
||||
);
|
||||
apply(&mut content, true);
|
||||
assert_eq!(content.text(), "one alpha\r\ntwo beta\nthree gamma");
|
||||
assert_eq!(content.selection(), Some(selected));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_op_outdent_preserves_semantic_word_selection_and_last_empty_line_is_excluded() {
|
||||
let mut content = Buffer::with_text("alpha tail\r\n");
|
||||
content.move_to(Cursor {
|
||||
position: at(0, 2),
|
||||
selection: None,
|
||||
});
|
||||
content.perform(Action::SelectWord);
|
||||
let raw = content.cursor();
|
||||
apply(&mut content, true);
|
||||
assert_eq!(content.cursor(), raw);
|
||||
assert_eq!(content.selection().as_deref(), Some("alpha"));
|
||||
content.perform(Action::SelectAll);
|
||||
apply(&mut content, false);
|
||||
assert_eq!(content.text(), " alpha tail\r\n");
|
||||
assert_eq!(content.cursor().position, at(1, 0));
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
//! Effective selected-byte ranges for a committed departure from a source view.
|
||||
//! Effective selected-byte ranges for committed source departures and edits.
|
||||
//!
|
||||
//! Iced's `Cursor` omits cosmic-text's Word/Line selection kind. Saving its raw
|
||||
//! endpoints can therefore save an empty range for a visibly selected word/line.
|
||||
//! Capture only after navigation admission and destination validation succeed,
|
||||
//! immediately before replacing/leaving the live editor. Never use this to inspect
|
||||
//! pending queries, failed/no-op navigation, or restore focus to the same editor.
|
||||
//! Departure capture requires navigation admission and destination validation,
|
||||
//! immediately before replacing/leaving the live editor. Edit capture requires
|
||||
//! an admitted edit with a nonempty patch plan. Neither is a passive query:
|
||||
//! never use these to inspect pending queries, failed/no-op navigation, or restore
|
||||
//! focus to the same editor.
|
||||
use iced::advanced::{graphics, text};
|
||||
use iced::widget::text_editor::{Action, Content, Cursor, Motion, Position};
|
||||
|
||||
@@ -21,6 +23,23 @@ use iced::widget::text_editor::{Action, Content, Cursor, Motion, Position};
|
||||
/// selection joins editor lines with LF regardless of physical CRLF/LF endings.
|
||||
/// Only the selected text is temporarily copied; retained state is one Cursor.
|
||||
pub fn capture<R>(content: &mut Content<R>) -> Cursor
|
||||
where
|
||||
R: text::Renderer<Editor = graphics::text::Editor>,
|
||||
{
|
||||
effective_range(content)
|
||||
}
|
||||
|
||||
/// Capture immediately before a committed indentation edit. Like departure,
|
||||
/// this converts Word/Line selections to their effective directional byte range.
|
||||
/// The caller restores the transformed range after editing the same Content.
|
||||
pub fn capture_for_edit<R>(content: &mut Content<R>) -> Cursor
|
||||
where
|
||||
R: text::Renderer<Editor = graphics::text::Editor>,
|
||||
{
|
||||
effective_range(content)
|
||||
}
|
||||
|
||||
fn effective_range<R>(content: &mut Content<R>) -> Cursor
|
||||
where
|
||||
R: text::Renderer<Editor = graphics::text::Editor>,
|
||||
{
|
||||
|
||||
@@ -329,8 +329,6 @@ fn reopening_canvas_for_a_modal_preserves_viewport_after_route_edits() {
|
||||
diagram,
|
||||
route: EdgeRoute {
|
||||
edge,
|
||||
source_attachment: None,
|
||||
target_attachment: None,
|
||||
waypoints: vec![Position { x: 400., y: 400. }],
|
||||
label: Some(EdgeLabel {
|
||||
fraction: 0.5,
|
||||
|
||||
@@ -112,8 +112,6 @@ mod tests {
|
||||
});
|
||||
view.routes.push(EdgeRoute {
|
||||
edge: exchange,
|
||||
source_attachment: None,
|
||||
target_attachment: None,
|
||||
waypoints: vec![Position { x: 420., y: 440. }],
|
||||
label: Some(EdgeLabel {
|
||||
fraction: 0.4,
|
||||
|
||||
@@ -466,12 +466,6 @@ fn diagram(app: &App) -> View<'_, Message> {
|
||||
zoom: app.zoom,
|
||||
pan: app.pan,
|
||||
context: app.route_context().unwrap(),
|
||||
selected_attachment: app
|
||||
.selected_attachment
|
||||
.filter(|(context, edge, _)| {
|
||||
Some(*context) == app.route_context() && *edge == app.selected
|
||||
})
|
||||
.map(|(_, _, end)| end),
|
||||
selected_bend: app
|
||||
.selected_bend
|
||||
.filter(|(edge, _)| *edge == app.selected)
|
||||
@@ -599,28 +593,6 @@ fn diagram(app: &App) -> View<'_, Message> {
|
||||
}
|
||||
if let Some(diagram) = app.active_diagram {
|
||||
let view = oa_core::projection::project_view(app.project(), diagram);
|
||||
if projection == Some(ProjectionKind::Traceability) {
|
||||
let correspondence_lines = view.edges.iter().filter(|edge| edge.trace).count();
|
||||
tools = tools.push(
|
||||
column![
|
||||
label("Shows OA and SA elements together, with correspondence lines alongside ordinary relationships."),
|
||||
label("Place both ends of a trace to show its line. Port traces use their owner cards; traces between relationships are reviewed on the Traceability page."),
|
||||
row![
|
||||
label(format!("{correspondence_lines} correspondence lines in this view")),
|
||||
action(
|
||||
"Add existing elements…",
|
||||
Message::Architecture(crate::app::architecture_authoring::Event::Existing(None))
|
||||
),
|
||||
action("Review layer traces", Message::ReviewLayerTraces),
|
||||
action("View visibility…", Message::Select(diagram)),
|
||||
]
|
||||
.spacing(7)
|
||||
.align_y(iced::Center)
|
||||
.wrap(),
|
||||
]
|
||||
.spacing(4),
|
||||
);
|
||||
}
|
||||
let edge = view.edges.iter().find(|edge| edge.id == app.selected);
|
||||
let promoted = edge.is_some_and(|edge| {
|
||||
oa_core::projection::edge_endpoint_placements(
|
||||
@@ -1206,6 +1178,15 @@ pub(crate) fn source_binding(
|
||||
use crate::source_navigation::{SymbolEvent, query::Action};
|
||||
use iced::keyboard::{Key, key::Named};
|
||||
use widget::text_editor::{Binding, Status};
|
||||
if matches!(press.status, Status::Focused { .. })
|
||||
&& press.key == Key::Named(Named::Tab)
|
||||
&& (press.modifiers.is_empty() || press.modifiers == iced::keyboard::Modifiers::SHIFT)
|
||||
{
|
||||
return Some(Binding::Custom(Message::SourceIndent(
|
||||
context.clone(),
|
||||
press.modifiers.shift(),
|
||||
)));
|
||||
}
|
||||
if matches!(press.status, Status::Focused { .. })
|
||||
&& press.modifiers.is_empty()
|
||||
&& press.key == Key::Named(Named::F2)
|
||||
|
||||
@@ -234,8 +234,6 @@ fn basic_transition_reconcile_keeps_existing_selected_view_placement_and_route_e
|
||||
.routes
|
||||
.push(EdgeRoute {
|
||||
edge: trace,
|
||||
source_attachment: None,
|
||||
target_attachment: None,
|
||||
waypoints: vec![Position { x: 301., y: 507. }],
|
||||
label: Some(EdgeLabel {
|
||||
fraction: 0.35,
|
||||
|
||||
@@ -12,8 +12,6 @@ use tokio::{sync::oneshot, task::JoinHandle};
|
||||
mod communication_limits;
|
||||
#[path = "http/data_contracts.rs"]
|
||||
mod data_contracts;
|
||||
#[path = "http/edge_attachments.rs"]
|
||||
mod edge_attachments;
|
||||
#[path = "http/port_anchors.rs"]
|
||||
mod port_anchors;
|
||||
#[path = "http/semantic_transitions.rs"]
|
||||
|
||||
@@ -1,344 +0,0 @@
|
||||
use super::*;
|
||||
use oa_core::{
|
||||
EdgeAttachment, PortAnchorSide,
|
||||
source_project::{ProjectLimits, SourceBundle, compile_bundle},
|
||||
};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
fn source_bundle() -> SourceBundle {
|
||||
compile_bundle(&SourceBundle {
|
||||
entry: "project.arcadia".into(),
|
||||
files: BTreeMap::from([
|
||||
("project.arcadia".into(), "// project rationale\narcadia 7; project attachments \"Attachments\" { include \"model.arcadia\"; include \"views.arcadia\"; }\n".into()),
|
||||
("model.arcadia".into(), "// semantic ownership\noa_activity first \"First\" {}\noa_activity second \"Second\" {}\ninteraction signal \"Signal\" { source @first; target @second; }\n".into()),
|
||||
("views.arcadia".into(), "// view rationale\ndiagram view \"View\" { routes [{ edge @signal; waypoints [{ x 120; y 80; }]; label { fraction 0.5; offset { x 3; y -7; }; }; }]; }\ndiagram other \"Other\" { routes [{ edge @signal; }]; }\n".into()),
|
||||
]),
|
||||
}, ProjectLimits::default()).unwrap().bundle
|
||||
}
|
||||
|
||||
fn id(project: &Project, alias: &str) -> Id {
|
||||
project
|
||||
.identities()
|
||||
.into_iter()
|
||||
.find(|(_, a)| *a == alias)
|
||||
.unwrap()
|
||||
.0
|
||||
}
|
||||
|
||||
async fn import(server: &TestServer) -> StoredProject {
|
||||
let response = server
|
||||
.client
|
||||
.post(server.url("/projects/import-source-project"))
|
||||
.json(&source_bundle())
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
let status = response.status();
|
||||
let value: Value = response.json().await.unwrap();
|
||||
assert_eq!(status, StatusCode::CREATED, "{value}");
|
||||
server
|
||||
.get(value["resource_id"].as_str().unwrap().parse().unwrap())
|
||||
.await
|
||||
}
|
||||
|
||||
async fn transact(
|
||||
server: &TestServer,
|
||||
current: &StoredProject,
|
||||
commands: Value,
|
||||
dry_run: bool,
|
||||
) -> reqwest::Response {
|
||||
server
|
||||
.client
|
||||
.post(server.url(&format!("/projects/{}/transactions", current.resource_id)))
|
||||
.header("if-match", current.etag())
|
||||
.json(&json!({"commands":commands,"dry_run":dry_run}))
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn assert_pair(current: &StoredProject) {
|
||||
let compiled = compile_bundle(¤t.source_project, ProjectLimits::default()).unwrap();
|
||||
assert_eq!(compiled.decoded.project, current.model);
|
||||
assert_eq!(compiled.bundle, current.source_project);
|
||||
}
|
||||
|
||||
async fn assert_unchanged(server: &TestServer, expected: &StoredProject) {
|
||||
let actual = server.get(expected.resource_id).await;
|
||||
assert_eq!(actual.etag(), expected.etag());
|
||||
assert_eq!(
|
||||
serde_json::to_value(actual).unwrap(),
|
||||
serde_json::to_value(expected).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn edge_attachments_http_dry_run_etags_source_parity_and_restart() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let server = TestServer::start(directory.path(), None).await;
|
||||
let original = import(&server).await;
|
||||
let diagram = id(&original.model, "view");
|
||||
let mut expected = original.model.clone();
|
||||
let route = &mut expected.diagrams.get_mut(&diagram).unwrap().routes[0];
|
||||
route.source_attachment = Some(EdgeAttachment {
|
||||
side: PortAnchorSide::Left,
|
||||
offset: 0.25,
|
||||
});
|
||||
route.target_attachment = Some(EdgeAttachment {
|
||||
side: PortAnchorSide::Bottom,
|
||||
offset: 1.0,
|
||||
});
|
||||
let commands = json!([Command::SetEdgeRoute {
|
||||
diagram,
|
||||
route: route.clone()
|
||||
}]);
|
||||
let preview = transact(&server, &original, commands.clone(), true).await;
|
||||
assert_eq!(preview.status(), StatusCode::OK);
|
||||
let preview: Value = preview.json().await.unwrap();
|
||||
assert_eq!(
|
||||
serde_json::from_value::<Project>(preview["model"].clone()).unwrap(),
|
||||
expected
|
||||
);
|
||||
let proposed: SourceBundle = serde_json::from_value(preview["source_project"].clone()).unwrap();
|
||||
assert_eq!(
|
||||
compile_bundle(&proposed, ProjectLimits::default())
|
||||
.unwrap()
|
||||
.decoded
|
||||
.project,
|
||||
expected
|
||||
);
|
||||
assert_unchanged(&server, &original).await;
|
||||
|
||||
let applied = transact(&server, &original, commands.clone(), false).await;
|
||||
assert_eq!(applied.status(), StatusCode::OK);
|
||||
let mut current = server.get(original.resource_id).await;
|
||||
assert_eq!(applied.headers()["etag"], current.etag());
|
||||
assert_eq!(current.revision, original.revision + 1);
|
||||
assert_eq!(
|
||||
current.model, expected,
|
||||
"only the two attachment fields in this view change"
|
||||
);
|
||||
assert_eq!(current.source_project, proposed);
|
||||
assert_pair(¤t);
|
||||
for path in ["project.arcadia", "model.arcadia"] {
|
||||
assert_eq!(
|
||||
current.source_project.files[path],
|
||||
original.source_project.files[path]
|
||||
);
|
||||
}
|
||||
assert!(current.source_project.files["views.arcadia"].contains("// view rationale"));
|
||||
let noop = transact(&server, ¤t, commands.clone(), false).await;
|
||||
assert_eq!(noop.status(), StatusCode::OK);
|
||||
assert_eq!(noop.headers()["etag"], current.etag());
|
||||
assert_eq!(noop.json::<Value>().await.unwrap()["changed"], false);
|
||||
let stale = json!([
|
||||
Command::Rename(id(¤t.model, "first"), "Must not persist".into()),
|
||||
commands[0].clone()
|
||||
]);
|
||||
assert_eq!(
|
||||
transact(&server, &original, stale, false).await.status(),
|
||||
StatusCode::PRECONDITION_FAILED
|
||||
);
|
||||
assert_unchanged(&server, ¤t).await;
|
||||
|
||||
let mut edited = current.source_project.clone();
|
||||
let text = edited.files.get_mut("views.arcadia").unwrap();
|
||||
assert_eq!(text.matches("offset 0.25;").count(), 1);
|
||||
*text = text.replace("offset 0.25;", "offset 0.375;");
|
||||
expected.diagrams.get_mut(&diagram).unwrap().routes[0]
|
||||
.source_attachment
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.offset = 0.375;
|
||||
let response = server
|
||||
.client
|
||||
.put(server.url(&format!("/projects/{}/source-project", current.resource_id)))
|
||||
.header("if-match", current.etag())
|
||||
.json(&edited)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
response.status(),
|
||||
StatusCode::OK,
|
||||
"{}",
|
||||
response.text().await.unwrap()
|
||||
);
|
||||
current = server.get(current.resource_id).await;
|
||||
assert_eq!(current.model, expected);
|
||||
assert_eq!(current.source_project, edited);
|
||||
assert_pair(¤t);
|
||||
let source = server
|
||||
.client
|
||||
.get(server.url(&format!("/projects/{}/source", current.resource_id)))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(source.status(), StatusCode::OK);
|
||||
assert_eq!(source.headers()["etag"], current.etag());
|
||||
assert_eq!(
|
||||
oa_core::language::parse(&source.text().await.unwrap()).unwrap(),
|
||||
expected
|
||||
);
|
||||
let bundle = server
|
||||
.client
|
||||
.get(server.url(&format!("/projects/{}/source-project", current.resource_id)))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(bundle.status(), StatusCode::OK);
|
||||
assert_eq!(bundle.headers()["etag"], current.etag());
|
||||
assert_eq!(
|
||||
bundle.json::<SourceBundle>().await.unwrap(),
|
||||
current.source_project
|
||||
);
|
||||
server.stop().await;
|
||||
|
||||
let server = TestServer::start(directory.path(), None).await;
|
||||
assert_unchanged(&server, ¤t).await;
|
||||
let mut route = expected.diagrams[&diagram].routes[0].clone();
|
||||
route.source_attachment = None;
|
||||
let reset = json!([Command::SetEdgeRoute {
|
||||
diagram,
|
||||
route: route.clone()
|
||||
}]);
|
||||
assert_eq!(
|
||||
transact(&server, ¤t, reset.clone(), false)
|
||||
.await
|
||||
.status(),
|
||||
StatusCode::OK
|
||||
);
|
||||
expected.diagrams.get_mut(&diagram).unwrap().routes[0] = route;
|
||||
let reset_state = server.get(current.resource_id).await;
|
||||
assert_eq!(
|
||||
reset_state.model, expected,
|
||||
"reset preserves pins, label, opposite end, other view and semantics"
|
||||
);
|
||||
assert_pair(&reset_state);
|
||||
let noop = transact(&server, &reset_state, reset, false).await;
|
||||
assert_eq!(noop.status(), StatusCode::OK);
|
||||
assert_eq!(noop.headers()["etag"], reset_state.etag());
|
||||
assert_eq!(noop.json::<Value>().await.unwrap()["changed"], false);
|
||||
server.stop().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn edge_attachments_http_malformed_and_invalid_offsets_reject_atomically() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let server = TestServer::start(directory.path(), None).await;
|
||||
let current = import(&server).await;
|
||||
let diagram = id(¤t.model, "view");
|
||||
let first = id(¤t.model, "first");
|
||||
for (bad, status) in [
|
||||
(json!({}), StatusCode::BAD_REQUEST),
|
||||
(json!({"side":"Top"}), StatusCode::BAD_REQUEST),
|
||||
(json!({"offset":0.5}), StatusCode::BAD_REQUEST),
|
||||
(
|
||||
json!({"side":"Diagonal","offset":0.5}),
|
||||
StatusCode::BAD_REQUEST,
|
||||
),
|
||||
(json!({"side":"Top","offset":null}), StatusCode::BAD_REQUEST),
|
||||
(
|
||||
json!({"side":"Top","offset":"NaN"}),
|
||||
StatusCode::BAD_REQUEST,
|
||||
),
|
||||
(
|
||||
json!({"side":"Top","offset":0.5,"unknown":true}),
|
||||
StatusCode::BAD_REQUEST,
|
||||
),
|
||||
(
|
||||
json!({"side":"Top","offset":-0.01}),
|
||||
StatusCode::UNPROCESSABLE_ENTITY,
|
||||
),
|
||||
(
|
||||
json!({"side":"Top","offset":1.01}),
|
||||
StatusCode::UNPROCESSABLE_ENTITY,
|
||||
),
|
||||
] {
|
||||
for field in ["source_attachment", "target_attachment"] {
|
||||
let mut route =
|
||||
serde_json::to_value(¤t.model.diagrams[&diagram].routes[0]).unwrap();
|
||||
route[field] = bad.clone();
|
||||
let commands = json!([Command::Rename(first, "Must roll back".into()), {"SetEdgeRoute":{"diagram":diagram,"route":route}}]);
|
||||
let response = transact(&server, ¤t, commands, false).await;
|
||||
assert_eq!(
|
||||
response.status(),
|
||||
status,
|
||||
"{field} {bad}: {}",
|
||||
response.text().await.unwrap()
|
||||
);
|
||||
assert_unchanged(&server, ¤t).await;
|
||||
}
|
||||
}
|
||||
let mut invalid = current.model.clone();
|
||||
invalid.diagrams.get_mut(&diagram).unwrap().routes[0].target_attachment =
|
||||
Some(EdgeAttachment {
|
||||
side: PortAnchorSide::Right,
|
||||
offset: 2.0,
|
||||
});
|
||||
let response = server
|
||||
.client
|
||||
.put(server.url(&format!("/projects/{}/model", current.resource_id)))
|
||||
.header("if-match", current.etag())
|
||||
.json(&invalid)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
|
||||
assert_unchanged(&server, ¤t).await;
|
||||
server.stop().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn edge_attachments_served_openapi_matches_required_nested_records_and_optional_ends() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let server = TestServer::start(directory.path(), None).await;
|
||||
let response = server
|
||||
.client
|
||||
.get(server.url("/openapi.json"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let schema: Value = response.json().await.unwrap();
|
||||
let schemas = &schema["components"]["schemas"];
|
||||
let attachment = &schemas["EdgeAttachment"];
|
||||
assert_eq!(attachment["required"], json!(["side", "offset"]));
|
||||
assert_eq!(attachment["additionalProperties"], false);
|
||||
assert_eq!(
|
||||
attachment["properties"]["side"]["$ref"],
|
||||
"#/components/schemas/PortAnchorSide"
|
||||
);
|
||||
assert_eq!(attachment["properties"]["offset"]["minimum"], 0);
|
||||
assert_eq!(attachment["properties"]["offset"]["maximum"], 1);
|
||||
for field in ["source_attachment", "target_attachment"] {
|
||||
assert!(
|
||||
!schemas["EdgeRoute"]["required"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.contains(&json!(field))
|
||||
);
|
||||
let optional = &schemas["EdgeRoute"]["properties"][field];
|
||||
assert_eq!(optional["default"], Value::Null);
|
||||
assert_eq!(
|
||||
optional["oneOf"],
|
||||
json!([{"$ref":"#/components/schemas/EdgeAttachment"},{"type":"null"}])
|
||||
);
|
||||
}
|
||||
let compiled = compile_bundle(&source_bundle(), ProjectLimits::default()).unwrap();
|
||||
let diagram = id(&compiled.decoded.project, "view");
|
||||
let mut route = compiled.decoded.project.diagrams[&diagram].routes[0].clone();
|
||||
let omitted = serde_json::to_value(&route).unwrap();
|
||||
assert!(omitted.get("source_attachment").is_none());
|
||||
assert!(omitted.get("target_attachment").is_none());
|
||||
route.target_attachment = Some(EdgeAttachment {
|
||||
side: PortAnchorSide::Right,
|
||||
offset: 0.0,
|
||||
});
|
||||
let encoded = serde_json::to_value(Command::SetEdgeRoute { diagram, route }).unwrap();
|
||||
assert_eq!(
|
||||
encoded["SetEdgeRoute"]["route"]["target_attachment"],
|
||||
json!({"side":"Right","offset":0.0})
|
||||
);
|
||||
server.stop().await;
|
||||
}
|
||||
+1
-28
@@ -2,7 +2,7 @@
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="generator" content="Docutils 0.23: https://docutils.sourceforge.io/" />
|
||||
<meta name="generator" content="Docutils 0.22.2: https://docutils.sourceforge.io/" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Arcadia Studio model REST API</title>
|
||||
<style type="text/css">
|
||||
@@ -947,33 +947,6 @@ authored files and revision. JSON/source imports may omit <span class="docutils
|
||||
it defaults to <span class="docutils literal">[]</span> and empty lists are omitted on export. The normative
|
||||
diagram semantics and source spelling are in <span class="docutils literal">language.rst</span> section 5.</p>
|
||||
</section>
|
||||
<section id="diagram-connection-attachments">
|
||||
<h2>Diagram connection attachments</h2>
|
||||
<p><span class="docutils literal">EdgeRoute.source_attachment</span> and <span class="docutils literal">target_attachment</span> are optional
|
||||
<span class="docutils literal"><span class="pre">{"side":"Bottom","offset":0.25}</span></span> records. Side and a finite offset in
|
||||
<span class="docutils literal">[0,1]</span> are required within a supplied record; unknown fields are rejected.
|
||||
Absence/null means automatic placement. Omitted fields in older schema-3
|
||||
models retain that meaning. The normal transaction endpoint accepts:</p>
|
||||
<pre class="literal-block">{"commands":[{"SetEdgeRoute":{
|
||||
"diagram":"<diagram UUID>",
|
||||
"route":{"edge":"<relation or trace UUID>",
|
||||
"source_attachment":{"side":"Bottom","offset":0.25},
|
||||
"waypoints":[],"label":null}
|
||||
}}]}</pre>
|
||||
<p><span class="docutils literal">SetEdgeRoute</span> replaces the complete route entry. Clients changing one end
|
||||
must retain its other attachment, bends and label; omit/null only the intended
|
||||
attachment to restore that end's automatic position. <span class="docutils literal">RemoveEdgeRoute</span> resets
|
||||
all presentation for that edge. Both support ETag checking, dry runs, atomic
|
||||
validation and source-project writeback. Invalid offsets reject the whole
|
||||
transaction without changing files or revision.</p>
|
||||
<p>These are per-view presentation preferences, not a semantic reconnect. Fractions
|
||||
follow the projected shape, including moves/resizes; semantic reconnect retains
|
||||
them as preferences of the same edge-end slots. Real semantic ports take
|
||||
precedence, and promoted folded endpoints suspend authored routing. The complete
|
||||
boundary mapping, lifecycle and generated-view limitations are normative in
|
||||
<span class="docutils literal">language.rst</span> section 5. GET model/source, PUT source, desktop and transactions
|
||||
share this representation; no separate attachment endpoint exists.</p>
|
||||
</section>
|
||||
<section id="contextual-process-and-chain-topology">
|
||||
<h2>Contextual process and chain topology</h2>
|
||||
<p>Operational processes and system functional chains can combine work
|
||||
|
||||
@@ -329,37 +329,6 @@ authored files and revision. JSON/source imports may omit ``port_anchors``;
|
||||
it defaults to ``[]`` and empty lists are omitted on export. The normative
|
||||
diagram semantics and source spelling are in ``language.rst`` section 5.
|
||||
|
||||
Diagram connection attachments
|
||||
-------------------------------
|
||||
|
||||
``EdgeRoute.source_attachment`` and ``target_attachment`` are optional
|
||||
``{"side":"Bottom","offset":0.25}`` records. Side and a finite offset in
|
||||
``[0,1]`` are required within a supplied record; unknown fields are rejected.
|
||||
Absence/null means automatic placement. Omitted fields in older schema-3
|
||||
models retain that meaning. The normal transaction endpoint accepts::
|
||||
|
||||
{"commands":[{"SetEdgeRoute":{
|
||||
"diagram":"<diagram UUID>",
|
||||
"route":{"edge":"<relation or trace UUID>",
|
||||
"source_attachment":{"side":"Bottom","offset":0.25},
|
||||
"waypoints":[],"label":null}
|
||||
}}]}
|
||||
|
||||
``SetEdgeRoute`` replaces the complete route entry. Clients changing one end
|
||||
must retain its other attachment, bends and label; omit/null only the intended
|
||||
attachment to restore that end's automatic position. ``RemoveEdgeRoute`` resets
|
||||
all presentation for that edge. Both support ETag checking, dry runs, atomic
|
||||
validation and source-project writeback. Invalid offsets reject the whole
|
||||
transaction without changing files or revision.
|
||||
|
||||
These are per-view presentation preferences, not a semantic reconnect. Fractions
|
||||
follow the projected shape, including moves/resizes; semantic reconnect retains
|
||||
them as preferences of the same edge-end slots. Real semantic ports take
|
||||
precedence, and promoted folded endpoints suspend authored routing. The complete
|
||||
boundary mapping, lifecycle and generated-view limitations are normative in
|
||||
``language.rst`` section 5. GET model/source, PUT source, desktop and transactions
|
||||
share this representation; no separate attachment endpoint exists.
|
||||
|
||||
Contextual process and chain topology
|
||||
-------------------------------------
|
||||
|
||||
|
||||
+2
-45
@@ -1785,16 +1785,14 @@ the Reparent command. Moving, resizing, arranging, or folding an owner retains
|
||||
its authored sides and fractions. Frozen schema-1/schema-2 readers do not accept
|
||||
<span class="docutils literal">port_anchors</span>; their explicit migration supplies an empty list.</p>
|
||||
<p>Each <span class="docutils literal">routes</span> entry is an EdgeRoute record with a required <span class="docutils literal">edge</span> reference
|
||||
to an existing Relation or Trace, <span class="docutils literal">waypoints</span> (default <span class="docutils literal">[]</span>), <span class="docutils literal">label</span>
|
||||
(default <span class="docutils literal">none</span>), and <span class="docutils literal">source_attachment</span> / <span class="docutils literal">target_attachment</span> (each
|
||||
default <span class="docutils literal">none</span>). Route records have no separate UUID or alias. An edge occurs
|
||||
to an existing Relation or Trace, <span class="docutils literal">waypoints</span> (default <span class="docutils literal">[]</span>), and <span class="docutils literal">label</span>
|
||||
(default <span class="docutils literal">none</span>). Route records have no separate UUID or alias. An edge occurs
|
||||
at most once in a diagram's route list; different diagrams MAY store independent
|
||||
routes for the same edge. List order and explicit empty entries are retained by
|
||||
source and JSON round trips, although list order has no routing meaning. Edges
|
||||
need not currently be visible in their diagram.</p>
|
||||
<pre class="code text literal-block"><code>routes [
|
||||
{ edge @report_exchange;
|
||||
source_attachment { side Bottom; offset 0.25; };
|
||||
waypoints [{ x 420; y 260; }, { x 690; y 260; }];
|
||||
label { fraction 0.55; offset { x 0; y -16; }; };
|
||||
}
|
||||
@@ -1811,47 +1809,6 @@ are structural errors. A pinned point that obstructs routing remains stored;
|
||||
geometric routing difficulty is reported separately from structural validity.
|
||||
Each EdgeRoute counts as one record against <span class="docutils literal">ParseLimits.max_records</span>; pinned
|
||||
points are bounded by the per-route limit and aggregate input byte limits.</p>
|
||||
<p>An attachment is an EdgeAttachment record with required <span class="docutils literal">side</span> (Left, Right,
|
||||
Top, Bottom) and required finite binary32 <span class="docutils literal">offset</span> from zero through one.
|
||||
Unknown fields, omitted nested fields and out-of-range offsets are structural
|
||||
errors (<span class="docutils literal">invalid_attachment_offset</span> for the latter). Attachments are bounded
|
||||
inline values, at most two per route, not additional graph objects or records.
|
||||
JSON uses the same field names, enum strings and number values. Omitted or null
|
||||
attachments mean automatic positioning; canonical compact source and JSON omit
|
||||
absent attachments. Earlier schema-3 input hydrates both fields as absent.</p>
|
||||
<p>The end names designate stored source/target slots, even for undirected edges.
|
||||
These values MUST NOT change semantic endpoints, ports, ownership, type,
|
||||
orientation or identity. Each preference belongs to that edge end in that view:
|
||||
another view remains independent. It follows the current endpoint's projected
|
||||
boundary under movement, resize or semantic reconnect; reconnect preserves the
|
||||
preference, as it preserves authored bends. The fraction does not identify the
|
||||
previous semantic endpoint. Deletion/reidentification of the edge follows the
|
||||
existing whole-route rules; moving, hiding or replacing its placement retains
|
||||
the preference. Removing one attachment restores that end's automatic position
|
||||
without changing bends, labels or the opposite attachment.</p>
|
||||
<p>For Left/Right, offset gives the Y fraction of the complete projected bounding
|
||||
box; for Top/Bottom it gives the X fraction. The attachment intersects the painted
|
||||
shape along that cardinal scan line. Rounded cards use radius 10, limited by half
|
||||
their width/height; Regions and fork/join bars are rectangles. State circles use
|
||||
their ellipse boundary and Choice uses its diamond boundary. More precisely, let
|
||||
<span class="docutils literal">t = 2 * offset - 1</span> and <span class="docutils literal">D</span> be width for Left/Right or height for Top/Bottom.
|
||||
The inset from the selected bounding side is <span class="docutils literal">D/2 * (1 - <span class="pre">sqrt(1-t*t))</span></span> for an
|
||||
ellipse and <span class="docutils literal">D/2 * abs(t)</span> for a diamond. A Terminate X uses the outermost
|
||||
painted-stroke intersection, inset <span class="docutils literal">D * min(offset, <span class="pre">1-offset)</span></span>. Zero and one
|
||||
are valid; different side/fraction pairs can describe the same painted point.
|
||||
Routing leads extend in the selected cardinal direction beyond the bounding box
|
||||
before turning. Source/API fractions retain their precision; pointer editing
|
||||
snaps fractions to hundredths without changing the stored semantic record.</p>
|
||||
<p>An actual semantic Port remains authoritative, including when projection hides
|
||||
its port marker. An edge attachment on that end is retained but dormant; use
|
||||
the port's own placement anchor to move a port. When folding promotes an endpoint
|
||||
to an ancestor, all authored route data is suspended and resumes on expansion.
|
||||
Geometric difficulty MUST NOT discard preferences. Coincident authored self-loop
|
||||
tips retain their positions with an external loop and a routing warning; the
|
||||
editor must offer an explicit way to choose either end even when handles overlap.
|
||||
Internal state transitions have no external connection. Process's generated
|
||||
topology and Scenario's lifeline/activation/time-derived tips are not consumers
|
||||
of these diagram route fields; this feature does not redefine their semantics.</p>
|
||||
<p><span class="docutils literal">SetEdgeRoute { diagram, route }</span> upserts an entry by edge identity, retaining
|
||||
the order and contents of unrelated entries. <span class="docutils literal">RemoveEdgeRoute { diagram, edge }</span>
|
||||
removes it; removing an absent entry is a no-op for an existing diagram and edge.
|
||||
|
||||
+2
-49
@@ -1167,9 +1167,8 @@ its authored sides and fractions. Frozen schema-1/schema-2 readers do not accept
|
||||
``port_anchors``; their explicit migration supplies an empty list.
|
||||
|
||||
Each ``routes`` entry is an EdgeRoute record with a required ``edge`` reference
|
||||
to an existing Relation or Trace, ``waypoints`` (default ``[]``), ``label``
|
||||
(default ``none``), and ``source_attachment`` / ``target_attachment`` (each
|
||||
default ``none``). Route records have no separate UUID or alias. An edge occurs
|
||||
to an existing Relation or Trace, ``waypoints`` (default ``[]``), and ``label``
|
||||
(default ``none``). Route records have no separate UUID or alias. An edge occurs
|
||||
at most once in a diagram's route list; different diagrams MAY store independent
|
||||
routes for the same edge. List order and explicit empty entries are retained by
|
||||
source and JSON round trips, although list order has no routing meaning. Edges
|
||||
@@ -1179,7 +1178,6 @@ need not currently be visible in their diagram.
|
||||
|
||||
routes [
|
||||
{ edge @report_exchange;
|
||||
source_attachment { side Bottom; offset 0.25; };
|
||||
waypoints [{ x 420; y 260; }, { x 690; y 260; }];
|
||||
label { fraction 0.55; offset { x 0; y -16; }; };
|
||||
}
|
||||
@@ -1198,51 +1196,6 @@ geometric routing difficulty is reported separately from structural validity.
|
||||
Each EdgeRoute counts as one record against ``ParseLimits.max_records``; pinned
|
||||
points are bounded by the per-route limit and aggregate input byte limits.
|
||||
|
||||
An attachment is an EdgeAttachment record with required ``side`` (Left, Right,
|
||||
Top, Bottom) and required finite binary32 ``offset`` from zero through one.
|
||||
Unknown fields, omitted nested fields and out-of-range offsets are structural
|
||||
errors (``invalid_attachment_offset`` for the latter). Attachments are bounded
|
||||
inline values, at most two per route, not additional graph objects or records.
|
||||
JSON uses the same field names, enum strings and number values. Omitted or null
|
||||
attachments mean automatic positioning; canonical compact source and JSON omit
|
||||
absent attachments. Earlier schema-3 input hydrates both fields as absent.
|
||||
|
||||
The end names designate stored source/target slots, even for undirected edges.
|
||||
These values MUST NOT change semantic endpoints, ports, ownership, type,
|
||||
orientation or identity. Each preference belongs to that edge end in that view:
|
||||
another view remains independent. It follows the current endpoint's projected
|
||||
boundary under movement, resize or semantic reconnect; reconnect preserves the
|
||||
preference, as it preserves authored bends. The fraction does not identify the
|
||||
previous semantic endpoint. Deletion/reidentification of the edge follows the
|
||||
existing whole-route rules; moving, hiding or replacing its placement retains
|
||||
the preference. Removing one attachment restores that end's automatic position
|
||||
without changing bends, labels or the opposite attachment.
|
||||
|
||||
For Left/Right, offset gives the Y fraction of the complete projected bounding
|
||||
box; for Top/Bottom it gives the X fraction. The attachment intersects the painted
|
||||
shape along that cardinal scan line. Rounded cards use radius 10, limited by half
|
||||
their width/height; Regions and fork/join bars are rectangles. State circles use
|
||||
their ellipse boundary and Choice uses its diamond boundary. More precisely, let
|
||||
``t = 2 * offset - 1`` and ``D`` be width for Left/Right or height for Top/Bottom.
|
||||
The inset from the selected bounding side is ``D/2 * (1 - sqrt(1-t*t))`` for an
|
||||
ellipse and ``D/2 * abs(t)`` for a diamond. A Terminate X uses the outermost
|
||||
painted-stroke intersection, inset ``D * min(offset, 1-offset)``. Zero and one
|
||||
are valid; different side/fraction pairs can describe the same painted point.
|
||||
Routing leads extend in the selected cardinal direction beyond the bounding box
|
||||
before turning. Source/API fractions retain their precision; pointer editing
|
||||
snaps fractions to hundredths without changing the stored semantic record.
|
||||
|
||||
An actual semantic Port remains authoritative, including when projection hides
|
||||
its port marker. An edge attachment on that end is retained but dormant; use
|
||||
the port's own placement anchor to move a port. When folding promotes an endpoint
|
||||
to an ancestor, all authored route data is suspended and resumes on expansion.
|
||||
Geometric difficulty MUST NOT discard preferences. Coincident authored self-loop
|
||||
tips retain their positions with an external loop and a routing warning; the
|
||||
editor must offer an explicit way to choose either end even when handles overlap.
|
||||
Internal state transitions have no external connection. Process's generated
|
||||
topology and Scenario's lifeline/activation/time-derived tips are not consumers
|
||||
of these diagram route fields; this feature does not redefine their semantics.
|
||||
|
||||
``SetEdgeRoute { diagram, route }`` upserts an entry by edge identity, retaining
|
||||
the order and contents of unrelated entries. ``RemoveEdgeRoute { diagram, edge }``
|
||||
removes it; removing an absent entry is a no-op for an existing diagram and edge.
|
||||
|
||||
+1
-45
@@ -2137,59 +2137,15 @@
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"EdgeAttachment": {
|
||||
"type": "object",
|
||||
"description": "Per-view preference on one stored connection end, not a semantic endpoint. Offset is finite and measured along the complete projected bounding-box side. Real semantic ports take precedence; hidden/promoted geometry retains the preference. Reconnect preserves the preference on its source/target slot.",
|
||||
"properties": {
|
||||
"side": {
|
||||
"$ref": "#/components/schemas/PortAnchorSide"
|
||||
},
|
||||
"offset": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"description": "Left/Right: top-to-bottom fraction. Top/Bottom: left-to-right fraction. Shape-aware geometry derives the actual contour point."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"side",
|
||||
"offset"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"EdgeRoute": {
|
||||
"type": "object",
|
||||
"description": "One diagram's authored presentation for an existing Relation or Trace, including currently hidden edges. No separate identity. Stores optional endpoint preferences, pinned points and label placement; explicit empty routes are retained. Removing the route resets all authored presentation.",
|
||||
"description": "One diagram's authored route for an existing Relation or Trace, including currently hidden edges. No separate identity. Only pinned points are stored; explicit empty routes are retained.",
|
||||
"properties": {
|
||||
"edge": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Existing Relation or Trace identity, never a node placement."
|
||||
},
|
||||
"source_attachment": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/EdgeAttachment"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Absent or null means automatic. Omitted when serializing automatic presentation. Resetting this field preserves the other end, pins and label."
|
||||
},
|
||||
"target_attachment": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/EdgeAttachment"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Absent or null means automatic. Omitted when serializing automatic presentation. Resetting this field preserves the other end, pins and label."
|
||||
},
|
||||
"waypoints": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
||||
+365
-7
@@ -1,5 +1,64 @@
|
||||
# Architectural and product decisions
|
||||
|
||||
## Private mixed syntax boundary — 2026-09-23
|
||||
|
||||
Proceed with the [reviewed syntax-only slice](analysis-mixed-parser-review-20260923.md)
|
||||
on tested private source `10d7f334`, retaining current root `195ddbb` as an
|
||||
ordinary-compatibility reference. Direct comparison found the relevant ordinary
|
||||
syntax/include machinery equal or equivalent; whole-file drift chiefly concerns
|
||||
semantic/editor behavior. A full root merge is not a prerequisite for this
|
||||
private experiment, and neither whole language/updater file may replace the
|
||||
other during eventual integration.
|
||||
|
||||
One opaque complete-bundle result borrows the actual original files and owns
|
||||
the shared budget, canonical file table, ordinary occurrence positions and typed
|
||||
unresolved syntax. It supplies no admitted Project, Index or edit authority.
|
||||
Explicit IDs are a staging restriction; missing IDs reject before generation.
|
||||
Retain duplicate syntax occurrences until semantic uniqueness is checked.
|
||||
Ordinary signed numbers require byte adjacency and complete value spans;
|
||||
typed subtraction retains its separate grammar. Bound aggregate syntax storage
|
||||
without silently reinterpreting caller model-record or ordered-step limits.
|
||||
Propagate phase-check failures even where the Budget does not latch them.
|
||||
|
||||
The [proposal](analysis-mixed-parser-proposal-20260923.md) and review define the
|
||||
independent fixtures and rejected shortcuts. Same-pass unresolved syntax is a
|
||||
dependency of later mixed resolution/index/composition, not public source8
|
||||
activation or a substitute for whole-output admission. Source implementation,
|
||||
fresh review and runtime checks remain required.
|
||||
|
||||
## Private source composition decision — 2026-09-23
|
||||
|
||||
Adopt the opaque validated original-span rename batch as the next private
|
||||
analysis adapter step, with the [independent review's clarifications](analysis-original-span-review-20260923.md).
|
||||
The [original proposal](analysis-original-span-proposal-20260922.md) compares
|
||||
this against replacing both ordinary and typed edit representations. The narrow
|
||||
adapter preserves current ordinary compatibility and reduces migration risk;
|
||||
a shared edit AST remains a candidate if structural composition demonstrates a
|
||||
need. Neither sequentially applying producers with stale offsets nor canonical
|
||||
rewriting is acceptable for authored-source preservation.
|
||||
|
||||
The immediate patch exports complete borrowed rename records and splits
|
||||
ownership validation from standalone final-output size preflight. It may defer
|
||||
only hypothetical typed-only output sizing; active inherited budget, plan/name
|
||||
storage, qualified namespace, source equality and complete occurrence checks
|
||||
remain mandatory. Standalone rename/apply semantics remain intact. This is
|
||||
private staging, not a complete parse-session or public source8 boundary.
|
||||
|
||||
Before production composition, one checked parsed-bundle witness must bind the
|
||||
complete source/entry/file map, ordinary model, typed index, profile and complete
|
||||
target intent. Ordinary syntax-kind replacement currently emits same-start
|
||||
insertion/deletion pairs; explicitly normalize such owned operations with exact
|
||||
trivia oracles before admitting them, keeping cross-producer collisions errors.
|
||||
Gate supported changes by a precise complete-model diff. Compile the actual
|
||||
emitted full bundle, use normal admission, and compare the complete intended
|
||||
Project before installation. Exact untouched bytes are an independent gate.
|
||||
Subtree edits, parameter lifecycle, repaired deletion, reidentity, relocation,
|
||||
missing-ID stabilization, migration/history and checked-save remain required
|
||||
delivery work; passing the private rename slice cannot close those requirements.
|
||||
|
||||
The source-pinned proposal/review retain their original local provenance labels.
|
||||
The accepted underlying repair contract is [r2 at analysis source 1b1c3e3](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/src/commit/1b1c3e3abb6fc3d0934c9dbfbd21fddc8ae4f594/harness/analysis-source8-20260913/analysis-source8-repair-design-r2-20260913.md).
|
||||
|
||||
## Current continuation decisions — 2026-09-06
|
||||
|
||||
- Saving under another source entry basename is destination bookkeeping, not an engineering edit. Adopt an exact current save receipt without adding Undo or clearing Redo; rebase uniform entry names across retained history after collision and compilation checks. Stale receipts cannot redirect newer raw source. Native discovery and eleven focused regression results are in release-save-as-runtime-packaging.md; updated native verification remains required.
|
||||
@@ -457,11 +516,310 @@ not new random identities assigned on each parse. Public source8 integration mus
|
||||
thread the same accounting through the outer project and included files; this
|
||||
private checkpoint does not itself admit a complete version8 source bundle.
|
||||
|
||||
## Connection-end presentation preferences — 2026-09-22
|
||||
## Complete mixed Project before typed editing index — 2026-09-23
|
||||
|
||||
Issue #33 uses optional side/fraction attachments on each view's `EdgeRoute`,
|
||||
with semantic Port precedence and shared shape-aware routing. Visual attachment
|
||||
editing remains separate from semantic reconnect. The [decision and evidence
|
||||
map](edge-attachments-20260922.md) records alternatives, lifecycle rules,
|
||||
independent falsifiers, evaluators and the Process/Scenario applicability limits.
|
||||
This is branch-specific work; it does not establish an integrated release.
|
||||
Choose the [private Project-first contract](analysis-mixed-project-contract-20260923.md)
|
||||
as the next semantic dependency, conditional on acceptance of its frozen syntax
|
||||
prerequisite. Reuse ordinary construction and typed lowering once, then check the
|
||||
complete Project and retain original syntax, the global lowering-site arena and
|
||||
its file ownership for later indexing. Including the typed index now would also
|
||||
change source-view/updater compatibility and several simultaneous allocation
|
||||
phases, while still leaving ordinary occurrence editing incomplete. This split
|
||||
preserves a concrete no-rescan/no-relower continuation and isolates model
|
||||
correctness from later edit authority; it does not narrow the required parity goal.
|
||||
|
||||
The [independent critique](reviews/mixed-project-contract-20260923.md) requires a
|
||||
full hand-authored retained-site oracle and a single admission result preserving
|
||||
static usage and every warning. Existing raw architectural diagnostic production
|
||||
is explicitly outside preallocation accounting; new mapped/retained storage must
|
||||
be reserved before copying or retention. Revisit the split if actual ownership
|
||||
work shows sites cannot be retained without duplicated models or repeated parsing.
|
||||
Public source8, mixed editing, checked-save and root reconciliation remain later
|
||||
dependencies, not capabilities delivered by this design decision.
|
||||
|
||||
## 2026-09-24 — preserve mixed bundle authority through index construction
|
||||
|
||||
Adopt the [mixed typed-index contract](analysis-mixed-index-contract-20260924.md):
|
||||
consume the sealed complete-Project witness into a distinct read-only owner and
|
||||
reuse private structural/token verification, with aggregate retained-ledger
|
||||
accounting. A copied verifier would duplicate correction work; reparsing into
|
||||
the old numeric-source Index would discard bundle/profile authority. The
|
||||
[independent review](analysis-mixed-index-review-20260924.md) resolved storage
|
||||
domain and completeness-proof ambiguities before adoption. This is a bounded
|
||||
implementation decision, not runtime acceptance or full editing parity. Revisit
|
||||
if a concrete verifier invariant prevents reuse or multiple retained owners
|
||||
become necessary; no generic public indexing framework is justified now.
|
||||
|
||||
## 2026-09-24 — index ordinary authored names before rename composition
|
||||
|
||||
Adopt the [ordinary occurrence successor](analysis-ordinary-index-contract-20260924.md)
|
||||
after source mapping and independent critique. Retain the accepted typed owner
|
||||
and add a physical-token-keyed ordinary map from sealed original positions.
|
||||
This isolates complete coverage/ownership from later edit intent and avoids a
|
||||
new structural path arena or parser. A fused streaming rename planner could
|
||||
save map storage, but would combine two unverified boundaries; revisit after
|
||||
coverage is established and a real consumer demonstrates the benefit.
|
||||
The contract explicitly handles synthetic brace-containing roots, complete
|
||||
reference extents and the existing ordinary depth policy. It grants read-only
|
||||
name-occurrence authority only. Rename composition and complete editing parity
|
||||
remain separate required capabilities.
|
||||
|
||||
## 2026-09-24 — gate mixed rename intent against the complete desired model
|
||||
|
||||
Adopt the [mixed rename planning contract](analysis-mixed-rename-plan-contract-20260924.md)
|
||||
after source mapping and independent criticism. Consume the complete authored
|
||||
owner, immutably borrow desired Project, and retain only changed-name and physical
|
||||
edit maps. Exhaustive shallow alias exceptions avoid Project clones, generic
|
||||
diff machinery and duplicate legacy batches. Final namespaces are checked
|
||||
simultaneously, so swaps work and collisions with unchanged names reject.
|
||||
|
||||
Existing whole-model PartialEq semantics define non-name equivalence, including
|
||||
signed-zero equivalence across geometry, scalar and historical carriers. Original
|
||||
non-name source bytes remain exact. The allocating iterative Expr comparator
|
||||
requires a conservative E scratch reservation; independent type-graph and
|
||||
frontier analysis bounds E by the retained original expression arena, including
|
||||
all four root families. Revisit if new Expr carriers appear or conservative
|
||||
headroom demonstrably blocks useful cases.
|
||||
|
||||
Deliberate later output parsing must preserve exact inherited per-snapshot policy
|
||||
with separately reported new usage and aggregate live-storage accounting. An
|
||||
independent fresh ledger must not hide retained output or old state. Execution
|
||||
ownership/readmission seams remain separate required implementation; this
|
||||
decision does not label a read-only plan as accepted rewritten source.
|
||||
|
||||
## 2026-09-24 — carry output storage through actual verification
|
||||
|
||||
The [execution source map](analysis-mixed-rename-execution-20260924/RENAME-EXECUTION-SOURCE-MAP.md)
|
||||
identifies the next boundary: output buffers already coexist with parsing, so
|
||||
adding a retained-output baseline only at resolution checks too late. Preserve
|
||||
the exact per-snapshot Limits and ProjectLimits, explicitly drop consumed old
|
||||
auxiliary state, and carry surviving output storage through verification.
|
||||
The final comparison must include aliases and parameter names; the planning
|
||||
gate deliberately omits them and cannot establish accepted rewritten output.
|
||||
|
||||
We considered authenticating exact output bytes, then reserving a conservative
|
||||
parser envelope derived from the unchanged syntax topology. The
|
||||
[independent feasibility investigation](analysis-mixed-rename-execution-20260924/RENAME-EXECUTION-ENVELOPE-FEASIBILITY.md)
|
||||
found useful partial bounds but no complete safe, useful aggregate formula.
|
||||
Finishing it would duplicate parser lifecycle knowledge across expression
|
||||
tasks, name sets, projection buffers, string decoding and include traversal.
|
||||
Select explicit carried storage accounting at actual growth/handoff seams for
|
||||
the next bounded design instead. Revisit an envelope only if a small, stable
|
||||
source-derived proof demonstrates useful caps; do not use unexplained byte or
|
||||
node multipliers. This chooses a direction, not an implemented execution API.
|
||||
|
||||
The [domain audit](analysis-mixed-rename-execution-20260924/CARRIED-STORAGE-DOMAIN-AUDIT.md)
|
||||
keeps the declared metric explicit: logical auxiliary slots/text are distinct
|
||||
from model/serde/raw-validator heap, allocator capacity and process RSS. The
|
||||
exploratory allocation inventory is not a requirement to meter all heap usage.
|
||||
The successor contract must reconcile that domain before selecting growth
|
||||
hooks or claiming an aggregate pre-allocation guarantee.
|
||||
|
||||
## 2026-09-25 — preserve parser compatibility while carrying live storage
|
||||
|
||||
Adopt the [private carried-phase prerequisite](analysis-carried-phase-contract-20260925.md).
|
||||
Keep legacy parser checkpoints unchanged; an explicit carried path checks the
|
||||
combined syntax and retained storage at the declared phase boundaries, including
|
||||
when its carried baseline is zero. An opaque guard binds the actual bundle,
|
||||
exact policy and retained owner through parsing and resolution. Seed the existing
|
||||
resolver ledger once, without charging live storage as new parse work.
|
||||
|
||||
New continuation constructors remain fixture-only until the actual output
|
||||
producer and consumed-owner transition exist. The narrow owning fixture contains
|
||||
only the bundle, so its carried count is F map entries, zero lower occurrences
|
||||
and all source/key/entry UTF-8 bytes. This is a phase-accounting prerequisite,
|
||||
not fulfillment or waiver of stronger parser producer timing requirements for
|
||||
final output acceptance. It grants no editing authority.
|
||||
|
||||
Runtime review of the first carried-phase candidate exposed a stack overflow in
|
||||
an unchanged legacy 128 KiB control during numeric parsing. Remove duplicated
|
||||
policy from the retained guard: the consumed input transfers exact policy into
|
||||
the Budget and syntax limits, while a small guard retains the backing owner.
|
||||
This changes representation without new heap storage or weaker tests. Prefer
|
||||
this to boxing policy or introducing an external policy-owner lifetime. The
|
||||
debugger identifies the active parsing path; it does not prove a sole cause or
|
||||
establish that the representation repair passes before it is actually tested.
|
||||
|
||||
## 2026-09-25 — separate owned emission from accepted rewritten output
|
||||
|
||||
The [emission contract](analysis-mixed-emission-contract-20260925.md) consumes
|
||||
the validated plan. A no-op returns its original owner; changed emission reserves
|
||||
the complete output map and UTF-8 storage while the old plan is live, then copies
|
||||
each file once in original edit coordinates. It drops old auxiliary ownership
|
||||
before constructing the private pending result. Exact policies and the desired
|
||||
whole Project survive, but no parse, adoption, save or public editing authority
|
||||
is granted. Reject any borrowed carried owner on the changed path: dropping a
|
||||
guard cannot free the externally owned allocation. A future owning transition
|
||||
must consume that allocation or account for its continued lifetime explicitly.
|
||||
|
||||
The [source-bound Windows evidence](experiments/analysis-mixed-emission-20260925/README.md)
|
||||
preserves an initial full-run stack abort and the successful repair. Split the
|
||||
existing iterative binding driver's prefix and continuation operations rather
|
||||
than changing arithmetic, increasing stack sizes, boxing state or weakening
|
||||
tests. Independent review establishes mechanical equivalence; actual rebuilt
|
||||
prologues and unchanged runtime controls support the bounded stack reduction.
|
||||
This does not close the older, separately unrerun full-pipeline setup limitation.
|
||||
|
||||
Final acceptance still requires parsing and resolving actual output bytes,
|
||||
complete Project equality under justified scratch bounds, explicit parser
|
||||
producer timing and an owner retaining the accepted bundle. Original expression
|
||||
counts alone do not bound an arbitrary faulty output. Logical auxiliary usage
|
||||
continues to exclude the established model/serde/validator and allocator domains;
|
||||
no process-wide memory guarantee follows from this private milestone.
|
||||
|
||||
## 2026-09-25 — reserve named syntax text at its producer
|
||||
|
||||
The [text producer contract](analysis-text-producer-contract-20260925.md) selects
|
||||
a separate private route bound to actual carried authority. Reserve raw quoted
|
||||
payload P before the existing decoder, then transfer decoded D and release P-D.
|
||||
This avoids a duplicate decoder while deliberately allowing conservative
|
||||
escaped-input refusal on the new route. Preserve legacy/phase-only diagnostics.
|
||||
Count simultaneous key/reference copies and all consumed UUID spellings; drop
|
||||
temporary strings before refund. Borrow the ordinary declaration-kind word.
|
||||
Keep small scalar state, transfer retained text once into the resolver ledger,
|
||||
and independently reconcile it against the existing syntax inventory.
|
||||
|
||||
The [verified Windows packet](experiments/analysis-text-producer-20260925/README.md)
|
||||
records six new passes, unchanged prior outcomes, initial lint repair and the
|
||||
single framework-hint difference in one inherited failure log. Preserve the
|
||||
original checker refusal and narrow named-byte amendment instead of weakening
|
||||
general comparison. Compiler prologue observations and existing stack controls
|
||||
bound evidence only; this adds no universal heap or stack guarantee.
|
||||
Entry/metadata growth timing and the remaining scratch inventory require their
|
||||
own explicit decision before actual emitted-output acceptance can be enabled.
|
||||
|
||||
## 2026-09-25 — strengthen the existing retained-entry metric at named producers
|
||||
|
||||
The [entry producer contract](analysis-entry-producer-contract-20260925.md)
|
||||
preserves U's declared grammar events and the conservative M inventory. A new
|
||||
explicit route checks C+U+M before selected growth; old routes retain their
|
||||
timing. Prefer grammar scalar counts plus a prepaid projector quota to another
|
||||
unmetered AST-counting frontier. Seven-slot declaration envelopes already include
|
||||
synthetic bodies; actual nested fields/values contribute separately. Quota
|
||||
consumption neither recharges nor refunds M, and independent lengths/metadata
|
||||
must reconcile. No saturating correction may create a successful witness.
|
||||
|
||||
[Windows evidence](experiments/analysis-entry-producer-20260925/README.md)
|
||||
preserves both precompile latch fixes, the syntax-cap versus counting-scratch
|
||||
evaluator correction and the initial test-only Debug-bound compile failure.
|
||||
All six new tests pass with unchanged inherited dispositions and strict Clippy.
|
||||
Additional scalar state changes compiler frames, so old stack controls and
|
||||
actual prologues remain evidence requirements; a logical slot is not a native
|
||||
stack byte. Remaining independently bounded parser scratch requires an explicit
|
||||
scope decision, not an inferred universal heap-metering requirement.
|
||||
|
||||
## 2026-09-25 — accept actual emitted bytes into one owning result
|
||||
|
||||
The [acceptance contract](analysis-mixed-acceptance-contract-20260925.md) selects
|
||||
a shared borrow of the complete sealed PendingRename as carried authority,
|
||||
rather than bare counters, duplicated policy or a new heap wrapper. Compile-time
|
||||
covariance permits the short verification borrow. Use both selected parser
|
||||
producer modes, complete resolution/admission and full actual-left Project
|
||||
PartialEq; reserve the actual expression arena count, not original provenance.
|
||||
|
||||
Move the actual model and mapped warnings with the accepted source. This preserves
|
||||
diagnostic/source identity without reparsing or substituting the desired model.
|
||||
Warnings already occupy the resolver ledger: exhaustively count their surviving
|
||||
inventory without charging twice, verify final O+W as a bounded subset while all
|
||||
verification owners remain live, then drop syntax/guards and move the bundle.
|
||||
No mutable extraction, history installation or repeated carried edit is implied.
|
||||
|
||||
Retain independently bounded traversal/delimiter/Pratt/duplicate-name scratch as
|
||||
an explicit separate domain. File/source-byte bounds do not tightly bound copied
|
||||
path bytes. This selected logical accounting is not universal heap accounting.
|
||||
The [finite Windows evidence](experiments/analysis-output-acceptance-20260925/README.md)
|
||||
checks exact inherited limits, real returned models/warnings, lifetime and stack
|
||||
behavior. Preserve the initial two incorrect empty-warning expectations and their
|
||||
independent full-warning correction. A raw build-target path spelling difference
|
||||
requires only a named exact-pair checker amendment, not weakened failure or source
|
||||
comparison. All earlier outcomes remain preserved; public integration is next.
|
||||
|
||||
## 2026-09-25 — restore ordinary schema8 compatibility before mixed adoption
|
||||
|
||||
Current formatters already emit schema8, while the ordinary reader previously
|
||||
rejected it. Admit that version through the existing ordinary grammar and model
|
||||
validation, preserving migration, complete source indexes, identity stabilization,
|
||||
updates and checked saves. Keep typed and long analysis-constructor refusals;
|
||||
ordinary acceptance does not qualify missing typed formatting or mixed editing.
|
||||
The [contract and independent comparison](ordinary-source8-compatibility-contract-20260925.md)
|
||||
reject replacing compile_bundle with an incomplete read-only index.
|
||||
|
||||
[Windows evidence](experiments/ordinary-source8-20260925/README.md) exercises the
|
||||
previously blocked nested/transaction workflows, complete ordinary OA/SA and
|
||||
historical trace round trips, relevant integration tests and actual read-only CLI
|
||||
commands. Preserve the initial example-trace assumption failure, four obsolete
|
||||
private header-rejection assertions and the historical JSON oracle's missing
|
||||
empty definitions catalog. Correct their expectations explicitly while retaining
|
||||
whole-model and typed-refusal checks; two test renames have exact inventory maps.
|
||||
|
||||
Reuse the source-identical core, other integration and CLI evidence after the last
|
||||
three-line migration-test correction. Verify the entire successor diff and every
|
||||
other declared input, rerun the affected target plus all-target Clippy/format, and
|
||||
identify each tested code version. No Linux, GUI or release qualification follows
|
||||
from these core/CLI results. Publication remains blocked by server disk exhaustion.
|
||||
|
||||
## 2026-09-25 — expose complete mixed validation through a borrowed read-only result
|
||||
|
||||
Select `language::check_mixed_bundle` with an opaque non-Clone result borrowing
|
||||
the caller's complete explicit-ID SourceBundle. Retain the real resolved owner
|
||||
and return immutable Project/warning views; do not substitute a bare model or an
|
||||
incomplete editable index. The [contract](analysis-public-read-contract-20260925.md)
|
||||
records alternatives, policy and bounds. Fresh input selects both existing
|
||||
producer modes with zero carried ownership, followed by complete resolution and
|
||||
admission under the exact caller policy. Internal entry ceilings remain separate
|
||||
from final-model record limits.
|
||||
|
||||
Promote only the required parser/resolver dependency cone to production; private
|
||||
index/edit machinery stays test-only. Normal-library tests are required because
|
||||
the production Parser layout changes even when ordinary callers select no mixed
|
||||
mode. That build exposed a missed semantic-row initializer, fixed at its source;
|
||||
two actual compiler warnings received narrow cfg/import corrections.
|
||||
|
||||
[Windows evidence](experiments/analysis-public-read-20260925/README.md) at bab6f96
|
||||
checks complete independently authored OA/SA models, exact warnings and refusals,
|
||||
source conservation, constructor policy, source lifetimes, prior workflows and
|
||||
actual ordinary CLI behavior. All 299 core and 75 selected integration cases pass.
|
||||
Rustdoc's two compile-fail runtime names append a suffix absent from its listing:
|
||||
preserve the recorder refusal and reconcile exactly those two names against the
|
||||
successful three-case raw run. No generic name normalization or rerun is needed.
|
||||
|
||||
This developer-facing API does not enable mixed file discovery in the CLI or
|
||||
graphical editing. Add those through explicit subsequent contracts, retaining
|
||||
filesystem safety and complete model/index ownership. Do not route a nominal
|
||||
mixed check through the ordinary loader, which currently rejects typed syntax
|
||||
and constructs editable compilation. Source/evidence were subsequently published
|
||||
after server storage recovery (issue7 comment1333); no release or Linux
|
||||
qualification follows.
|
||||
|
||||
## 2026-09-25 — separate mixed CLI discovery from editable ordinary loading
|
||||
|
||||
Expose `arcadia check-mixed ROOT [--json]` through a closed read-only dispatch and
|
||||
opaque mixed source snapshot. Reuse the real parser and existing disk checks,
|
||||
but do not construct or expose an ordinary editable index for typed input.
|
||||
The [contract](analysis-mixed-cli-contract-20260925.md) specifies explicit
|
||||
identities, source8, report fields, original locations and exit behavior.
|
||||
|
||||
Share one discovery Budget/ModelCounts across included files and reserve the
|
||||
file metadata before parsing. Retain producer counters conservatively after
|
||||
each temporary syntax unit drops. This is cumulative discovery reservation,
|
||||
not a claim about live allocations. Drop that phase before complete semantic
|
||||
validation under a fresh budget with the original caller policy. Clamp the
|
||||
source-byte ceiling before disk reads. Stop on the first located mixed parsing
|
||||
failure so a span-only budget latch cannot be attributed to a later sibling;
|
||||
retain final filesystem and pending-journal checks.
|
||||
|
||||
[Evidence](experiments/analysis-mixed-cli-20260925/README.md) at f10490b1 checks
|
||||
whole independent OA/SA models, exact reports and bytes, cumulative limits,
|
||||
ordinary migrations/saves and finite stack controls. New public disk and CLI
|
||||
tests passed initially; two old CLI assertions still expected schema7. Correct
|
||||
only their exact migration/header expectations, retaining the immutable fixture,
|
||||
all40 identities and full-text/model/fixed-point checks. Alias the test process
|
||||
Command import to remove the observed warning. Preserve those original outcomes.
|
||||
|
||||
Independent review also found the actual alarm include_str fixture missing
|
||||
from the inherited recorder's input recipe. Add that precise path before final
|
||||
gates (567 inputs); keep the six initial 566-input receipts and their omission
|
||||
caveat. Final source-bound checks pass 302 core,98 integrations,3 docs, strict
|
||||
core/CLI Clippy and5 direct CLI runs. Formatting remains the same inherited
|
||||
failure. Publication recovered without changing the existing release; mixed
|
||||
editing, application adoption and Linux qualification remain separate work.
|
||||
|
||||
+378
-19
@@ -1,20 +1,233 @@
|
||||
# Verified branch checkpoint — 2026-09-13 UTC
|
||||
# Published Windows preview and branch checkpoint — 2026-09-24 UTC
|
||||
|
||||
This describes Codex-956atf's contribution, not a combined release.
|
||||
The published preview below identifies the combined delivery. Earlier
|
||||
contribution checkpoints retain their original source and acceptance scope.
|
||||
[PROJECT_GOAL.md](../PROJECT_GOAL.md) remains authoritative. The complete OA/SA
|
||||
application is unfinished; published previews and finite prototypes are intermediate.
|
||||
|
||||
**Linux/WSL builds and checks remain deferred until the user explicitly reports
|
||||
networking resolved.** No guest setup, diagnosis, restart or cache transfer is
|
||||
active. The VPN remains untouched.
|
||||
**The current prerelease is Windows-only. Linux/WSL builds and checks remain
|
||||
excluded by the user's explicit request, including after networking recovers.**
|
||||
|
||||
## Current integrated candidate and evidence
|
||||
Published [integrated Windows preview](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/releases/tag/v0.2.0-preview.integrated.20260924):
|
||||
**v0.2.0-preview.integrated.20260924**, release7, prerelease=true, draft=false.
|
||||
The tag, exact release body and all seven attachment identities were read back
|
||||
after publication. The 34,168,661-byte Studio ZIP has SHA256
|
||||
`5554e32533e3cb2c33fb4143a5a0902733d5a8d47577303cb2842d268e7bc76d`;
|
||||
the release includes its checksum, two companion kits/checksums and VALIDATION.md.
|
||||
[Release notes](prerelease-integrated-20260924-release-notes.md) and
|
||||
[final validation](prerelease-integrated-20260924-validation.md) identify exact
|
||||
commands, evidence, launch steps, known limits and the two machines' bounded
|
||||
packaged trials. Full application and broad human-checklist acceptance remain
|
||||
unfinished; this development-profile preview is ready for human verification.
|
||||
|
||||
The published Windows source is
|
||||
**715036e132677dd2bc2fb37ed316d93dc9987aec** on
|
||||
`codex/prerelease-integrated-956atf-20260924`. Its
|
||||
[source handoff](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/src/commit/715036e132677dd2bc2fb37ed316d93dc9987aec/harness/prerelease-integrated-source-20260924.md)
|
||||
identifies the integrated contributions, reviewed event correction and exact
|
||||
test paths. Combined runtime/package acceptance is separate from the
|
||||
contribution-specific evidence below. [Issue37](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/37)
|
||||
owns current build and release coordination.
|
||||
The Route helper qualification and Allocation Text Apply test import corrections
|
||||
are included; both earlier compile failures occurred before tests ran. The candidate's 66 package
|
||||
documents/examples pass [closure and source-identity verification](reviews/prerelease37-windows-documents-20260924.md).
|
||||
The separate e68a839 Route contribution's raw test/lint evidence passed an
|
||||
[independent evidence review](reviews/prerelease37-route-evidence-20260924.md);
|
||||
its results do not establish combined-candidate execution.
|
||||
The full Windows test run at `8ddfadf4` failed a State picker fixture whose
|
||||
canvas-relative coordinates hit the added selection toolbar. The current
|
||||
source includes peer `a9cd364e`'s measured picker slot and capture assertion;
|
||||
the [independent source review](reviews/prerelease37-state-picker-20260924.md)
|
||||
retains its bounded assumptions. [Reviewed targeted evidence](reviews/prerelease37-state-picker-evidence-20260924.md)
|
||||
at `a9cd364e` passes formatting and all26 State tests, including the original
|
||||
failure. The corrected combined [test-evidence review](reviews/prerelease37-final04-tests-20260924.md)
|
||||
at `715036e` verifies 2,200 passed, eight ignored and zero failed across all
|
||||
2,208 names in 61 native harnesses, including all 29 required cases. The
|
||||
[complete gate review](reviews/prerelease37-final04-complete-20260924.md) verifies
|
||||
all 3,709 inputs against immutable Git bytes, strict workspace Clippy and the
|
||||
individual Studio/server builds. The supplemental combined build also passes;
|
||||
its differing artifacts are not the selected package inputs. Packaged acceptance
|
||||
remains separate. The [downloaded package](reviews/prerelease37-downloaded-package-20260924.md)
|
||||
passes exact archive/product/document checks. [Server evidence](reviews/prerelease37-package-server-20260924.md)
|
||||
verifies 16 requests and exact model/source/revision/ETag persistence across an
|
||||
owned forced restart; the initial legacy-schema fixture expectation failure is
|
||||
preserved. [Packaged GUI evidence](reviews/prerelease37-native-gui-20260924.md),
|
||||
[separate-machine QA](reviews/prerelease37-independent-qa-20260924.md) and the
|
||||
[parent visual cross-check](reviews/prerelease37-parent-visual-20260924.md) pass
|
||||
their bounded reviewed scopes. State label congestion, explicit transition-form
|
||||
Cancel and the preserved launcher refusals remain described in final validation.
|
||||
|
||||
## Published read-only mixed source CLI — 2026-09-25
|
||||
|
||||
Source **f10490b13bb3332ff60c2372d554e276f6f44959** is pushed and ref-verified
|
||||
on `codex/analysis-mixed-cli-956atf-20260925`. `arcadia check-mixed ROOT [--json]`
|
||||
discovers explicit-ID schema8 files with the existing filesystem safeguards,
|
||||
then validates the complete ordinary and typed model. Reports preserve original
|
||||
source locations; the command does not change files. Ordinary CLI commands retain
|
||||
their separate migration and editing behavior.
|
||||
|
||||
The [contract](analysis-mixed-cli-contract-20260925.md) and
|
||||
[Windows evidence](experiments/analysis-mixed-cli-20260925/README.md) identify
|
||||
302 core passes, 98 integration passes across eleven suites, three doctests,
|
||||
strict core/CLI Clippy and five direct CLI trials. Exact source bytes, report
|
||||
contents, unchanged file inventories and eleven prior small-stack controls were
|
||||
independently reconciled. The original two stale schema7 test expectations and
|
||||
import warning remain in the evidence, alongside their test-only repairs.
|
||||
The final 567-input recipe includes the alarm fixture omitted by the initial
|
||||
566-input recipe; the initial omission is explicitly retained as a limitation.
|
||||
Formatting still reports the identical pre-existing differences.
|
||||
|
||||
Discovery and final validation have separate bounded budgets; cumulative
|
||||
discovery reservations are conservative and do not establish an all-heap bound.
|
||||
This checkpoint supplies no mixed editing/save/history, graphical adoption,
|
||||
atomic filesystem snapshot or universal depth128 guarantee. The existing
|
||||
Windows prerelease is unchanged, and Linux remains deferred. Live continuation
|
||||
and the user's requested pause belong in issue7, not this checkpoint.
|
||||
|
||||
## Published public mixed source8 read API — 2026-09-25
|
||||
|
||||
Source **bab6f96a5b05b37f2b5d67a430aed0f67d783e73** on
|
||||
`codex/analysis-public-read-956atf-20260925` exposes
|
||||
`language::check_mixed_bundle(&SourceBundle, ProjectLimits)`. The result borrows
|
||||
the complete explicit-ID input and exposes only its admitted Project and located
|
||||
warnings. It retains the existing syntax, storage and admission evidence; no
|
||||
editable index, save permission or CLI discovery is implied.
|
||||
|
||||
The [contract](analysis-public-read-contract-20260925.md) and
|
||||
[Windows evidence](experiments/analysis-public-read-20260925/README.md) record
|
||||
299 core passes, ten new normal-library integration passes, 65 existing ordinary
|
||||
integration passes, three documentation cases, strict core Clippy and actual
|
||||
ordinary CLI check/rename-preview success at this exact source. All 298 prior
|
||||
core outcomes and the named small-stack controls pass. Format retains identical
|
||||
pre-existing differences. The first missing production initializer, subsequent
|
||||
warning cleanup and exact rustdoc recorder-name reconciliation are preserved.
|
||||
|
||||
Source and evidence were published after server storage recovery; all five
|
||||
backlog source refs and evidence ref `f810fb40` were read back successfully.
|
||||
[Issue7 handoff](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/7#issuecomment-1333)
|
||||
confirms publication and identifies the separately pending mixed CLI acceptance.
|
||||
The released preview is unchanged. Mixed CLI discovery is supplied above; graphical/editor adoption,
|
||||
typed saving/history and the broader application remain unfinished. Existing
|
||||
depth qualification and full-pipeline small-stack limitations remain; Linux/WSL
|
||||
checks remain user-deferred.
|
||||
|
||||
## Published ordinary source8 compatibility — 2026-09-25
|
||||
|
||||
Source **7b06b8ea1b7ef902ee0b3d0ec2971a9a7a1f1acd** on
|
||||
`codex/ordinary-source8-956atf-20260925` restores public ordinary parsing of the
|
||||
schema8 files already emitted by current formatters. Versions3–7 still migrate;
|
||||
typed analysis declarations remain explicitly unsupported by ordinary APIs.
|
||||
The [contract](ordinary-source8-compatibility-contract-20260925.md) and
|
||||
[Windows evidence](experiments/ordinary-source8-20260925/README.md) distinguish
|
||||
this compatibility repair from full mixed-source editing.
|
||||
|
||||
The unchanged implementation at **0c07f53ab517626f536f79dbaa2885a587bf1db7**
|
||||
passes all298 core tests, including the16 formerly header-blocked workflows,
|
||||
and60 selected integration tests. Actual CLI check and cross-file rename preview
|
||||
pass with exact reported changes and unchanged input files. The successor only
|
||||
repairs a historical JSON expectation and its note; its five migration tests and
|
||||
strict core all-target Clippy pass. Format retains identical pre-existing
|
||||
differences. Evidence preserves all three failed evaluator stages, two explicit
|
||||
test-name mappings and exact component-version reuse. Source and evidence are
|
||||
published after server recovery; the published preview is unchanged.
|
||||
|
||||
## Earlier private parser and output acceptance checkpoints — 2026-09-25
|
||||
|
||||
Source **edf096cd33350865ecfc3c65ec266a6c0d3c4000** on
|
||||
`codex/analysis-mixed-acceptance-956atf-20260925` connects real emitted output to
|
||||
combined parser producers, full model admission/equality and an owning result
|
||||
containing the actual bundle, model and mapped warnings.
|
||||
[Windows evidence](experiments/analysis-output-acceptance-20260925/README.md)
|
||||
records eleven new passes, strict core Clippy and all 281 prior outcomes preserved:
|
||||
the full 292-name run gives 276 passes/16 inherited failures. All 16 failure bodies remain
|
||||
identical after numeric thread-identifier normalization; format is unchanged.
|
||||
Ten inherited stack controls and new scoped 128 KiB verification/drop pass. Initial
|
||||
nine-pass/two-failure warning-oracle corrections and the exact build-target path
|
||||
spelling exception are preserved. Source and evidence are published. Public
|
||||
typed read validation is supplied above; installation/history and repeated
|
||||
carried editing remain incomplete.
|
||||
Ordinary source8 compatibility is supplied by the later checkpoint above.
|
||||
|
||||
The earlier **29417130c71f70c4eaefe8ed4c879efba78eb72c** on
|
||||
`codex/analysis-entry-producers-956atf-20260925` adds the reviewed
|
||||
[retained-entry producer route](analysis-entry-producer-contract-20260925.md).
|
||||
Its [Windows evidence](experiments/analysis-entry-producer-20260925/README.md)
|
||||
passes six new cases and strict core Clippy. Full library remains failed at
|
||||
265 passed/16 inherited failed, preserving all275 prior outcomes, all16 failure
|
||||
bodies (numeric thread identifiers only normalized) and all ten stack controls. Formatting is
|
||||
unchanged. Initial test-only compilation failure is preserved. This source and
|
||||
packet are published after server storage recovery.
|
||||
Actual output verification/accepted ownership is supplied by the later checkpoint
|
||||
above; public editing remains open.
|
||||
|
||||
Source **97a8703adc75467a7f8e75f5c6cb00ca542b707c** on
|
||||
`codex/analysis-text-producers-956atf-20260925` adds the separate private
|
||||
[syntax text producer route](analysis-text-producer-contract-20260925.md).
|
||||
Its [source-bound Windows evidence](experiments/analysis-text-producer-20260925/README.md)
|
||||
passes six new cases and strict core Clippy. Full library remains failed at
|
||||
259 passed /16 inherited failed; all269 prior outcomes and all ten stack controls
|
||||
are preserved. Format remains the same inherited failure. Failure output is
|
||||
15 exact blocks plus one precisely documented missing framework hint.
|
||||
|
||||
This source and evidence are published after server storage recovery, with
|
||||
remote evidence commit `f810fb40` verified alongside the source branch;
|
||||
emission source `8d52dab9` is published separately. This checkpoint does not
|
||||
change the integrated preview. The later acceptance contract explicitly selects
|
||||
the remaining scratch domain and implements actual output verification/ownership;
|
||||
its exclusions and public-integration limits remain material.
|
||||
|
||||
## Earlier local contribution checkpoint and evidence
|
||||
|
||||
Branch `codex/behavior-authoring-20260910`, published as `codex/release1-956atf`.
|
||||
Current source **c28abcf13a834585a9954f7e54175d47c0a0460b** adds Find focus and
|
||||
departure-selection corrections. Combined execution and native acceptance are
|
||||
not yet established; [integration issue27](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/27)
|
||||
holds current verification and release coordination.
|
||||
Integrated code checkpoint **4ffff0d9d94bd1875e6b316b84ac11e5c5c9657c** combines
|
||||
the reviewed ordinary/Text contribution **3f9e6a18af96b5cd10940da4093be8da28b9c835**
|
||||
with source-editor Tab/Shift+Tab indentation. Windows desktop verification passes
|
||||
1,188 tests with six existing ignores; all 1,194 names and nine targeted
|
||||
indentation cases are accounted for. Strict desktop Clippy and matching GUI
|
||||
build pass on 711 unchanged inputs. The closed native trials cover caret and
|
||||
forward/reverse block indentation, text undo/redo, empty drafts, continued caret
|
||||
typing and Apply/Save/reopen. The saved 577 bytes exactly preserve the baseline's
|
||||
mixed line endings and Unicode while indenting its 13 physical lines. The first
|
||||
trial restored the baseline and is not a save oracle. Find's Tab does not advance
|
||||
focus; this inherited limitation is distinct from the verified editor boundary.
|
||||
This is Windows contribution acceptance, not complete editor accessibility,
|
||||
public source8, a combined peer union, or a packaged release.
|
||||
[Exact gates, original native captures and independent review](experiments/text-indentation-4ffff0d-20260923/README.md)
|
||||
retain both the failed initial compilation and the interrupted first trial.
|
||||
|
||||
Earlier code checkpoint **8abc144cf233c64e43c6bee5033c22ee818e2d14** preserves
|
||||
the production inputs of **feab2020d8e5b6bfdb59c86ab4aee161617b8b6f**. Its Windows
|
||||
desktop suite passes 1,096 tests with three existing manual ignores; all 1,099
|
||||
names are admitted. Targeted navigation/rename checks, strict desktop Clippy and
|
||||
the matching development GUI build pass. The closed native trial verifies
|
||||
Details-to-Preview scroll restoration, explicit reset boundaries, and exact
|
||||
six-file preservation after Cancel and Save. The
|
||||
[portable evidence](experiments/rename-scroll-feab202-20260913/README.md), published
|
||||
at **f95d0310482565e91220a97e13fc756f27f9d590**, retains the initial failing test,
|
||||
20 original frames and independent review. Its committed bytes and verifier were
|
||||
rechecked on September 22. This narrow trial does not establish Text integration,
|
||||
complete rename acceptance or release readiness.
|
||||
|
||||
Earlier production source follows; each result retains its original code scope.
|
||||
Verified production source **44095f78c28b79d6d085863dc928adba8e24d955** includes
|
||||
Find focus, departure-selection and scoped Escape corrections. Its Windows
|
||||
desktop suite passes1095 with3 existing manual ignores; core passes471 with1
|
||||
manual ignore; server passes56. All three strict Clippy scopes and the matching
|
||||
development GUI/server build pass. [Exact source, commands and raw evidence](release-navigation-440-windows-20260913.md).
|
||||
The integrated documentation checkpoint **3c22fe2629fc242714d32805790262fa2c4e33b9**
|
||||
preserves those production inputs and includes the updated AGENTS instructions.
|
||||
|
||||
The actual440 native trial closed normally with79 notes,78 images and14 complete
|
||||
saved-model/source comparisons. Rename/Undo/Redo/reopen, scoped selection,
|
||||
placement/state targeting and sampled OA/SA lifecycle/topology navigation are
|
||||
verified within the [closed trial's explicit scope](experiments/navigation-native-440-20260913/REPORT.md).
|
||||
It found a same-page Details-to-Preview scroll-loss defect; initial keyboard
|
||||
Delete after chooser navigation also required an explicit canvas click. Neither
|
||||
is hidden by passing saved-byte checks. The scroll defect is corrected and
|
||||
verified by the separate feab trial above; the initial chooser-to-canvas keyboard
|
||||
focus friction remains a limitation of the 440 trial. This remains a development
|
||||
checkpoint, not a combined release. [Integration issue27](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/27)
|
||||
owns current verification, correction and release coordination.
|
||||
Earlier source **e1240295bd114d23d3f67a69e6b38bdb2e25bb0d** combines the canvas
|
||||
contribution with source/diagram navigation and reviewed alias rename. Its1082-test
|
||||
listing,17 cross-feature tests,4 mounted preview-scroll tests and strict all-target
|
||||
@@ -70,16 +283,162 @@ failures and corrections. Earlier authoring and allocation checkpoints remain in
|
||||
|
||||
## Isolated contributions and material limitations
|
||||
|
||||
Analysis/core source checkpoint **b2c970b4673e062e47eadd12906d2316c4b1a7b3**,
|
||||
Combined peer candidate **e34ba27e26ede3ab55f276acc250d92b2e0c3204**, published on
|
||||
`codex/peer-union-956atf-20260923`, composes current Text/Tab with saved Scenario,
|
||||
final inspector actions, attachments, State bends and the transition footer.
|
||||
The literal Windows workspace suite passes 2,005 tests with zero failures and
|
||||
eight named ignores; all 2,013 listed names across 60 executables and three
|
||||
doctest sections are accounted for. Strict workspace/all-target Clippy and the
|
||||
matching native/external-oracle builds pass on 760 unchanged inputs. The
|
||||
[portable gate evidence](experiments/peer-union-e34-windows-20260923/README.md)
|
||||
binds three rebuilt executables through fresh listings and preserves the failed
|
||||
historical f82 desktop run (1,337 passed, three failed, seven ignored), the
|
||||
missing-DLL listing attempt, and capture limitations. Its verifier passes the
|
||||
positive case and rejects all 12 semantic mutations. The preceding
|
||||
[partial gate archive](experiments/peer-union-windows-20260923/README.md) retains
|
||||
earlier Scenario and lint failures. The separate
|
||||
[native checkpoint](experiments/peer-union-e34-native-20260924/README.md) records
|
||||
33 successful whole-model/source checkpoints, the retained failed G2 pointer
|
||||
attempt and its recovery, and five transition checks through fresh reopen and
|
||||
unchanged reapply. The first trace identity and all resulting source-bundle bytes
|
||||
remain exact. Independent review checks 245 frame pins and inspects 38 keyframes;
|
||||
17 transition-oracle controls meet their expected outcomes. The original
|
||||
interrupted process and later normal-close observations remain distinct. Dirty
|
||||
middle-button pan, held-drag Escape and short-window Scenario Apply are unobserved.
|
||||
Public format and release acceptance remain incomplete; accepted root production
|
||||
remains4ffff0d. The newer prerelease candidate and live coordination are in
|
||||
[issue37](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/37).
|
||||
|
||||
Analysis/core source checkpoint **a2a28f8d23a46ba1696b6a8410d4d3b5c1808f49**,
|
||||
publication branch `codex/analysis-core-956atf-20260911`, is not integrated into
|
||||
production. It adds source-save decode/equality containment and a private staged
|
||||
expression/declaration parser, then repairs the frozen source3 lowering envelope.
|
||||
Its129-test library run has113 passes and16 failures, all at the unsupported
|
||||
source8 header. All22 expression/declaration/token cases and8 bridge/save guards
|
||||
pass; strict all-target Clippy passes on524 unchanged inputs. The separate language
|
||||
integration target has7 passes and9 failures:8 unsupported headers and an old V2
|
||||
oracle that omits the new empty definitions catalog. [Exact S2a evidence](release-analysis-source-s2a-20260913.md)
|
||||
preserves every failure and the independent structural comparison.
|
||||
production. Its private staged typed parser/lowerer has142 listed tests,
|
||||
34 passing typed cases,1 passing raw-token case and strict all-target core Clippy
|
||||
on531 unchanged inputs. The complete library run remains126 passed/16 failed,
|
||||
with all16 failures at the unsupported public source8 header. The original
|
||||
fixture error and its test-only correction remain in [exact S2b evidence](release-analysis-source-s2b-20260913.md).
|
||||
Published private writer **f1d79df4979121335ed7b4ce4da84b2fca8a9ef4** has
|
||||
[portable S2c evidence](experiments/analysis-source-s2c-windows-20260913/README.md):
|
||||
152 listed, 44 typed and one token test passed, strict Clippy passed; the full
|
||||
library result remains 136 passed / 16 failed at unsupported public source8.
|
||||
Published source **1b1c3e3abb6fc3d0934c9dbfbd21fddc8ae4f594** adds a private
|
||||
qualified source index/rename plan and the independently reviewed cleanup for
|
||||
the demonstrated depth129 rejected-tree stack overflow. Its
|
||||
[S3 evidence](experiments/analysis-source-s3-windows-20260922/README.md), published
|
||||
at **345a42e4658f6e0e8e01ef64a2df8f4e28a8945a**, records 167 listed tests,
|
||||
59 typed passes (nine index/five update), one token pass, three clean stack
|
||||
controls and strict Clippy on 538 unchanged inputs. The original overflow and
|
||||
dirty diagnostic/probe results are retained. No current full-core pass is
|
||||
claimed. Public source8 parser, writer, source-update and persistence composition
|
||||
remain incomplete; these branches are not integrated production analysis.
|
||||
|
||||
Published private source **10d7f3340d5cd61a656dc0bcf7c143f8897c5551** adds
|
||||
complete borrowed original-span export for later edit composition. Its
|
||||
[Windows evidence](experiments/analysis-original-span-20260923/README.md) records
|
||||
173 listed tests (all prior 167 retained), 65 typed passes including all 11
|
||||
updater cases, and strict core all-target Clippy on the same 538 committed
|
||||
inputs. Review caught and corrected an intermediate standalone budget
|
||||
regression. This is an isolated export adapter; the later syntax checkpoint
|
||||
below supplies mixed-bundle ownership, while output compilation and public
|
||||
source8 remain incomplete.
|
||||
|
||||
Published private mixed-syntax source
|
||||
**a578ac98a06772a60bec00ae287e65650680d119** retains the complete source bundle,
|
||||
original positions and exact caller policy, using shared ordinary/typed token
|
||||
consumption with explicit identities and aggregate file/byte admission. Its
|
||||
[Windows evidence](experiments/analysis-mixed-syntax-20260923/README.md) records
|
||||
194 listed tests, all prior 173 retained, 20 focused mixed passes and 85 typed
|
||||
passes within the actual full result: **178 passed / 16 failed**. All 152
|
||||
historical statuses are unchanged and all 42 later tests pass. Strict core
|
||||
all-target Clippy passes on 541 unchanged committed inputs. Formatting remains
|
||||
failed in six inherited, unchanged files. Original failed gates and the host
|
||||
clock discontinuity are retained; a fresh actual direct listing binds the final
|
||||
focused/full executable after Cargo rebuilt it. This syntax witness does not
|
||||
resolve or admit a complete mixed Project, activate public source8 or implement
|
||||
editing composition. The accepted
|
||||
[complete-Project contract](analysis-mixed-project-contract-20260923.md) defines
|
||||
the semantic boundary implemented by the separate private checkpoint below.
|
||||
|
||||
Published private complete-Project source
|
||||
**aaec78fcc9b273d1485497126392afdd1fb2e54e** retains the original syntax, bundle,
|
||||
budgets and limits while resolving/admitting the complete ordinary/typed model.
|
||||
The next [mixed typed-index contract](analysis-mixed-index-contract-20260924.md)
|
||||
is accepted after source review and resolved accounting/completeness findings.
|
||||
Private typed-index source **93eb6117f95353d25d594d59780018f197c51672** implements
|
||||
the consumed witness and shared verifier. Its
|
||||
[Windows evidence and independent oracle review](experiments/analysis-mixed-index-20260924/README.md)
|
||||
verify strict core Clippy, all seven new cases and the complete 216-name inventory
|
||||
on 545 unchanged inputs. Full library remains **200 passed / 16 failed**,
|
||||
preserving all 209 baseline statuses; formatting still has the identical eight
|
||||
inherited blocks. Original lint and depth-expectation test failures are preserved.
|
||||
The [source review](analysis-mixed-index-20260924/source-review.md) records three
|
||||
repaired diagnostic findings; its reviewed production hashes are unchanged.
|
||||
The new owner keeps legacy updater authority separate. Complete mixed editing
|
||||
and public source8 remain required.
|
||||
The [ordinary occurrence successor contract](analysis-ordinary-index-contract-20260924.md)
|
||||
uses retained parser positions and physical token keys, with a separate consumed
|
||||
read-only owner. Private source **ee500908e3d77182ddd54e12524f9b6c6eb43b60** now
|
||||
implements that dependency. Its [Windows evidence and independent review](experiments/analysis-ordinary-index-20260924/README.md)
|
||||
verify all13 new cases, strict core Clippy and the229-name inventory on549
|
||||
unchanged inputs. Full library remains **213 passed /16 failed**, with all216
|
||||
prior outcomes and failure bodies unchanged; formatting retains the identical
|
||||
eight inherited blocks. Initial9-pass/4-fail fixture evidence and exact test-only
|
||||
repairs are preserved. Both occurrence families now retain complete original
|
||||
authority, but no rename composition or public edit capability follows yet.
|
||||
The [whole-target rename planning contract](analysis-mixed-rename-plan-contract-20260924.md)
|
||||
is implemented privately at **18073a6f2eabce83fc83997f094e331eec835a16**.
|
||||
Its [Windows evidence and independent review](experiments/analysis-mixed-rename-plan-20260924/README.md)
|
||||
verify all 17 new tests and strict core Clippy on 551 unchanged inputs. The full
|
||||
246-name run remains **230 passed / 16 failed**, preserving all prior outcomes;
|
||||
formatting retains identical inherited failures. Complete non-name comparison,
|
||||
simultaneous namespaces, exact merged edits, inherited caps and combined sizing
|
||||
are verified. The scoped planner comparison/drop test passes on 128 KiB;
|
||||
separate full-pipeline parsing/setup overflows remain explicitly unresolved.
|
||||
The [carried-phase prerequisite](analysis-carried-phase-contract-20260925.md)
|
||||
is implemented privately at **fa7a980418ef99d772e67c66c28dfa7b5181f875**.
|
||||
Its [Windows evidence](experiments/analysis-carried-phase-20260925/README.md)
|
||||
records 12 new passes, strict core Clippy, 553 unchanged inputs and a completed
|
||||
258-name full run with **242 passed / 16 failed**. Exact policy, retained bundle
|
||||
ownership and aggregate phase accounting survive parsing and resolution;
|
||||
legacy checkpoints remain unchanged. An initial full-run stack regression was
|
||||
corrected by retaining a smaller guard, with the existing 128 KiB controls
|
||||
unchanged and passing. The earlier separate setup overflow remains unresolved.
|
||||
At that checkpoint, continuation constructors are fixture-only. The subsequent
|
||||
[private output-emission contract](analysis-mixed-emission-contract-20260925.md)
|
||||
is implemented at **8d52dab97c92cc31baae9f20d8c324b55818aa5d** on
|
||||
`codex/analysis-mixed-emission-956atf-20260925`.
|
||||
Its [Windows evidence](experiments/analysis-mixed-emission-20260925/README.md)
|
||||
records eleven new passes and a completed 269-name full run with **253 passed /
|
||||
16 unchanged failed**, strict core all-target Clippy and identical inherited
|
||||
formatting failures on 555 unchanged inputs. The consuming no-op preserves the
|
||||
original owner; changed output reserves all owned source storage before copying
|
||||
and remains private and unverified. Borrowed carried owners cannot use the changed
|
||||
path. An initial stack abort is preserved; a mechanical parser split reduces
|
||||
the observed numeric-prefix prologue reservations by 13,032 bytes, and the
|
||||
unchanged overflowing control passes the final suite and three fresh processes.
|
||||
Accepted editing and stronger parser allocation-timing obligations remain open.
|
||||
[DECISIONS.md](DECISIONS.md) records the scope and rejected
|
||||
alternatives; live ownership and acceptance remain in issue7.
|
||||
The preceding Project [Windows evidence](experiments/analysis-mixed-project-20260924/README.md)
|
||||
distinguishes runtime source **71cadfa0cd0e112caa29588d2494753301f3cb6c** from
|
||||
the formatting-only aaec successor. Strict core all-target Clippy, actual listing
|
||||
209, Project 13/13, shared validation 2/2 and four exact public compatibility
|
||||
cases pass on 544 unchanged inputs. Full lib remains **193 passed / 16 failed**,
|
||||
preserving all prior statuses and exact unsupported-public-source8 diagnostics.
|
||||
Successor formatting remains failed on eight inherited blocks in five unchanged
|
||||
files; no aaec runtime rerun is claimed. The parent-reviewed portable verifier
|
||||
and all 15 finite controls pass, with independent raw-output/pin readback.
|
||||
This private source is not integrated into the application prerelease. Public
|
||||
source8, mixed edit execution, checked output and UI parity remain
|
||||
unfinished.
|
||||
|
||||
The [reviewed S1/S7 assessment](issue28-s1-s7-assessment-956atf-20260923.md)
|
||||
distinguishes static analysis/case overlays from contextual inheritance,
|
||||
fixed/default bindings and executable trade studies with durable result
|
||||
provenance. Strengthened fixture and parity criteria remain proposals, not
|
||||
implemented evaluation or a replacement metamodel decision.
|
||||
Earlier [S2a evidence](release-analysis-source-s2a-20260913.md) retains its
|
||||
129-test113/16 library result and separate7/9 language integration result,
|
||||
including the old V2 oracle that omitted the empty definitions catalog.
|
||||
[Earlier bridge evidence](release-analysis-source3-bridge-20260913.md) remains scoped
|
||||
to6216cff.
|
||||
The earlier2c515cf full library run had98 passes and16 failures:14 rejected the
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
# Private carried-baseline phase-check contract
|
||||
|
||||
2026-09-25. **Selected private prerequisite; implementation and runtime acceptance remain pending.** Source inspected: `18073a6f2eabce83fc83997f094e331eec835a16`. This contract builds on [the domain audit](analysis-mixed-rename-execution-20260924/CARRIED-STORAGE-DOMAIN-AUDIT.md) and the accepted mixed-parser, mixed-Project, typed-index, ordinary-index and mixed-rename-plan contracts. It neither implements a parser nor grants rewritten-output acceptance.
|
||||
|
||||
## 1. Deliverable and nonclaims
|
||||
|
||||
Introduce one private way to parse a deliberate complete snapshot under the **exact inherited Limits and ProjectLimits**, while carrying a constructor-controlled live auxiliary baseline through existing parser phase checks and subsequent resolution. The resulting parsed/resolved witnesses keep that baseline; no separately passing fresh ledger may hide it.
|
||||
|
||||
The metric remains the existing logical triple `(entry_slots, lower_occurrence_slots, owned_in_scope_text_bytes)`. This dependency preserves current parser timing limits: it is **not before-every-allocation admission**, a complete parser-transient inventory, a raw model/serde/validator heap meter, an allocator-capacity/RSS bound, or a stack guarantee. Existing grammar entry checks, raw token/byte/Exact accounting and model/static policies remain intact.
|
||||
|
||||
This is independently useful because any later owned-output verifier needs exact policy and nonzero live-storage participation before it can safely compose existing parser/resolver phases. It is insufficient on its own to accept rewritten output. Emission reservations, actual-byte verification, whole-intent equality, returned ownership and installation/history remain separate successor requirements.
|
||||
|
||||
## 2. Ownership and construction authority
|
||||
|
||||
The production continuation is private and constructible only from an owning or borrow-bound retained-state witness with a verified ledger and exact policy. A scalar Slots argument alone is not authority. It contains the inherited policy and the scalar carried live Slots. It must not be Clone/Copy or expose a production constructor accepting arbitrary Limits, replacement Budget or arbitrary Slots. A scalar Slots type can remain Copy internally; the authority wrapping it cannot be independently duplicated through its API.
|
||||
|
||||
Selected scope: no production constructor or old-owner/emitter transition is delivered in this prerequisite. Both borrowed and retained construction remain in fixture-only helpers; the production-shaped parser/guard machinery is exercised without granting fabricated output authority. Test-only construction binds a real retained output-shaped fixture owner and its actual bundle; it demonstrates arithmetic and propagation, not an implemented emitter transition. A bare Slots argument plus an unrelated later bundle is forbidden even for the proposed production shape. The minimal concrete ownership seam is specified in §5.
|
||||
For actual later use, the transition must distinguish:
|
||||
|
||||
- **Old auxiliary state dropped:** consume and finish dropping old syntax/index/maps/warnings/plan-owned auxiliary data before constructing the verification continuation. Surviving output buffers/map/path keys/entry and any additional named auxiliaries supply C.
|
||||
- **Old auxiliary state retained:** C includes the still-live old baseline plus all surviving new output/auxiliary ownership. A mere change of phase cannot release old reservations.
|
||||
|
||||
The desired Project remains an external immutable borrow with the established model-allocation exclusion. An emitted SourceBundle must have a stable external owner throughout any syntax/result borrowing it. Do not return an owning bundle alongside a witness that borrows that same field. A future callback/scoped verifier or drop-syntax-then-own-output result requires its own accepted design.
|
||||
|
||||
The parser consumes the continuation and borrows the actual complete SourceBundle. Entry, canonical paths, exact file set/bytes and normal include/reachability authority still come from that bundle and the normal traversal. No replacement numeric file table, caller-supplied tokens/model, loose index or partial file subset is accepted. Failure returns diagnostics, no reusable continuation or partial witness; its external output owner remains responsible for dropping its own buffers.
|
||||
|
||||
## 3. Policy and disjoint counters
|
||||
|
||||
Let:
|
||||
|
||||
- C = live carried Slots inherited from the owning transition; fixed for this parse/resolution attempt unless a documented owned allocation is actually released.
|
||||
- U = current snapshot `Budget.usage.entries`.
|
||||
- M = current existing `ParsedMixedSyntax.metadata` subtotal.
|
||||
- T = current snapshot retained syntax text counted by the existing bounded syntax-accounting walk at resolution.
|
||||
- A = subsequent existing resolver/catalog/lower/diagnostic reservations and named accounting scratch.
|
||||
|
||||
Create one **new-snapshot** Budget from the exact captured Limits. Preserve all Limits fields, including tightened bytes/tokens/entries/references/frames/numeric_units, and the complete captured ProjectLimits. Do not reconstruct defaults from only caller bytes/references, silently widen, or replace the old live witness's Budget. Work Usage and numeric usage start at zero for this deliberate snapshot and accumulate normally across all included files.
|
||||
|
||||
C is live storage, not parse work: do not add it to bytes/tokens/entries/references Usage, Exact work, model records or deepest_frame. New source bytes are charged once during scanning even if those same bytes are also carried owned output text: these are distinct work and storage dimensions. Original input/output parse work reports remain distinct, never reset/subtracted to pretend the reparse did no work.
|
||||
|
||||
All arithmetic composing C with phase totals must use checked addition and refuse overflow. The entry ceiling is Limits.entries; occurrence ceiling remains the existing entries-plus-references ceiling; text ceiling remains Limits.bytes. Aggregate output byte size remains a separate source-size check. A failure from nonlatching lowering/index phase checks must propagate immediately; later `Budget::active()` is not a substitute.
|
||||
|
||||
## 4. Exact phase checks and temporal limits
|
||||
|
||||
Use the current syntax charge table without adding speculative containers. In particular M begins with file references and later adds the current `tokens.len()`-based metadata component, value-end subtotal, ordinary position slots and lexical-owner count (`mixed.rs:277–280,321–330`). The module header describes the token-count component as token-end metadata and tokens themselves as token-bounded; preserve the existing one component, **do not introduce a second tokens.len() charge** by relabeling it. Dropped value-end subtotal may remain the inherited conservative overestimate.
|
||||
|
||||
Required ordering/checkpoints for the explicit carried route (legacy wrappers retain their existing checks):
|
||||
|
||||
1. **Continuation admission:** after policy validation/Budget construction and before broad traversal allocation, check all three C dimensions against the exact inherited ceilings. Report overflow/cap failure safely at entry/default span.
|
||||
2. **Complete input size precheck:** retain current checked sum of all source file lengths and `index_bytes(total_source_bytes)` (`mixed.rs:269–276`). Do not sum borrowed input bytes into C.text again merely because they are being parsed. In the output-owned case their ownership is already in C.
|
||||
3. **Initial file metadata:** preserve the current file-slot precheck before `Traversal::new` (`:277–284`), replacing its independent entry check with `C.entries + M` (U is zero here).
|
||||
4. **Existing scan/grammar checks:** preserve current raw token, byte, syntax-entry, reference, depth and Exact event checks exactly. Do not add a new check on every allocation or change legacy parser semantics. C does not change these work counters.
|
||||
5. **Per-file preprojection metadata checkpoint:** after the parsed unit and existing `ordinary_position_slots` count, construct prospective M with checked arithmetic, then check **C.entries + U + M** before position projection (`:319–340`). This is the new combined aggregate phase check. It includes all completed prior files and current grammar work. It is not a proof that current-unit ASTs, tokens or counting scratch were never temporarily allocated before refusal.
|
||||
6. **Successful complete-syntax boundary:** after traversal succeeds and before returning the witness, recheck C plus final U+M, and C's other dimensions. Preserve exact carried C and scalar M in the sealed syntax result. No second reservation is created by a noncharging check.
|
||||
7. **Resolver transition, exactly once:** start its Ledger with C, then run the existing syntax-accounting admission once: reserve U+M and walk retained structures to reserve T and accounting-work slots. Continue all existing catalog, lowering, mapped-diagnostic and index reservations from that same ledger. Final resolver baseline is C+the normal resolved baseline, with the same existing releases. Do not add parser checks as reservations and charge U/M again; do not seed zero; do not run two syntax walks merely to recreate authority.
|
||||
|
||||
Parser-owned syntax Strings and selected parser temporaries retain their **existing timing**: retained text is admitted by the bounded walk after parsing, before resolver-dependent allocations. Traversal internals, position delimiter/end scratch and parser counting Vecs do not suddenly become individually Ledger-metered. They retain existing independent file/token/depth controls; no universal auxiliary-preallocation claim may be made. Accounting::syntax's own counting work remains explicitly reserved before its growth.
|
||||
|
||||
The resolver already declares model/serde and raw architecture-validator allocations outside this metric (`accounting.rs:1,72–79`; Project contract §3). Keep these exclusions. Produced raw Issues are admitted into charged retained diagnostics before destination retention; output/source-key Strings newly introduced by a later emitter are in scope and cannot use the exclusions to disappear.
|
||||
|
||||
## 5. Smallest concrete Rust ownership seam
|
||||
|
||||
Selected private types (names may follow repository conventions):
|
||||
|
||||
```rust
|
||||
struct SnapshotPolicy { limits: Limits, project_limits: ProjectLimits }
|
||||
|
||||
// Owns the actual bundle represented by its verified retained slots. Fields
|
||||
// private; no production nonzero constructor until the emitter transition.
|
||||
struct RetainedSnapshot {
|
||||
bundle: SourceBundle,
|
||||
policy: SnapshotPolicy,
|
||||
retained: Slots,
|
||||
}
|
||||
|
||||
enum SnapshotBacking<'s> {
|
||||
// External caller-owned input, exactly like the existing parser domain;
|
||||
// its source bytes are borrowed, so C is zero. Not an emitted-owned result.
|
||||
Borrowed { bundle: &'s SourceBundle, policy: SnapshotPolicy },
|
||||
Retained(&'s RetainedSnapshot),
|
||||
}
|
||||
|
||||
// Not Clone/Copy. Methods derive bundle, exact policy and C from the backing;
|
||||
// no independently supplied bundle or arbitrary C at parse invocation.
|
||||
struct CarriedInput<'s> { backing: SnapshotBacking<'s> }
|
||||
|
||||
fn parse_carried(input: CarriedInput<'_>) -> Result<ParsedMixedSyntax<'_>, Diagnostics>;
|
||||
```
|
||||
|
||||
`RetainedSnapshot` keeps the output map, source Strings, path keys and entry alive; retained counts correspond to that exact owner. It must not have a public or generally available `new(bundle, slots, policy)`. For this prerequisite, `cfg(test)` builds a real retained fixture bundle and counts its F entries and actual source/key/entry UTF-8 bytes internally. Its policy comes from the tested existing immutable witness/profile, not a widened replacement. Production support for emission must later reserve these allocations before copying and install verified counts through its sole transition constructor. No arbitrary other live old-owner allocations may be claimed by this minimal retained shape; if the future design keeps old auxiliaries, add an explicit guard for them then.
|
||||
|
||||
The borrowed variant supplies a fixture-only carried C=0 route without pretending an owned output is free. It captures exact policy through a narrow internal read-only policy snapshot of an existing sealed witness. There is no independent replacement profile at `parse_carried` invocation. Its bundle is fixed in the input. Test-only tighter-profile construction remains explicitly test-only. The two backing variants describe ownership, not a numeric-zero branch controlling checks.
|
||||
|
||||
Consume `CarriedInput<'s>` at parser entry, transferring its exact policy into the new Budget and existing syntax ProjectLimits field. Retain only a small `SnapshotGuard<'s>` distinguishing borrowed zero carry from a reference to the allocation-owning RetainedSnapshot. Do not duplicate the full policy inside every later syntax/resolved/index/plan value. The current bundle reference is derived from the same consumed input. This uses the existing bundle lifetime, so no self-reference or extra generic lifetime is needed. In retained mode both references borrow the external stable snapshot; the syntax cannot outlive it. Keep the small guard inside syntax when resolve consumes syntax and places it in `ResolvedMixedProject`. Read-only methods expose no way to remove or replace the guard. Baseline extraction for resolution is an internal scalar read, not a second independently transferable authority.
|
||||
|
||||
Use one shared parse body with an explicit legacy-versus-carried option. Existing parse wrappers select legacy and retain their old checkpoint timing. The carried entry always runs combined checks, including its borrowed C=0 variant. Do not select behavior with `C == 0`. A narrow resolver initialization obtains C from the guard (zero for legacy), seeds Ledger once, and invokes existing `accounting::syntax` once; existing resolved ownership already retains syntax and therefore the guard. No separate wrapper around every later index phase is necessary.
|
||||
|
||||
The current Budget keeps Limits private. Add only an internal read-only exact-policy snapshot seam if needed, not mutation or resetting of a live Budget. New snapshot Usage remains independent. Do not implement carry by subtracting C from Limits globally; that would conflate storage with source/token/reference/depth/numeric work. Do not duplicate grammar, include budgets, scans, model state or generic allocation infrastructure.
|
||||
|
||||
This representation deliberately postpones an owning accepted-output result and emitter. An actual emitted bundle must live in `RetainedSnapshot` (or an equally explicit future sealed owner) before verification borrows it. Parsing/resolution failures drop the temporary guard/witness; the surrounding owner then drops output normally. No accepted output or installation authority escapes merely because a carried parse/resolve succeeded.
|
||||
## 6. Observable acceptance through real paths
|
||||
|
||||
Except explicitly labeled arithmetic/fault-injection controls below, cases must use real `parse_with_profile`-equivalent parsing and `resolve`, not synthetic parser outputs or a separately fabricated accepted Project. Expected arithmetic and affected phase/location are independently authored. Do not mutate a live admitted owner to tighten its Budget.
|
||||
|
||||
1. **Zero-C compatibility:** accepted existing OA/SA, multiple canonical files, Unicode/CRLF, ordinary positions, warnings, Project, original bundle borrowing and parse/numeric Usage remain identical through full resolution/index. Compare opt-in carried C=0 successful outcomes with the original entry, while separately verifying the original path preserves its old refusal timing. Current ordinary callers preserve exact policy and prior phase behavior. Run existing syntax/project/index/rename tests and retain inherited failure dispositions. The earlier-refusal qualification below must be explicit.
|
||||
2. **Carried retention:** a small admitted bundle with independently hand-counted baseline C yields the same complete Project, warnings, syntax work/numeric usage and canonical file keys as zero C. Resolved live Slots differ by exactly C in each domain; lower temporary releases never release C. This output-shaped owner has C.occurrences = 0; occurrence overflow/cap checks may be fault-injection or direct arithmetic tests, not claims of a real retained occurrence owner.
|
||||
3. **Initial checkpoint:** C entries plus all-file references exactly fit/fail at cap/cap-minus-one before traversal; C text/occurrences each independently fit/fail without relabeling source bytes or references. Use realistic retained fixture storage for positive cases. Dedicated arithmetic-overflow tests may use test-only max values.
|
||||
4. **Combined C+U+M checkpoint:** author a real minimal/multi-file fixture where each independent C, U and M fits but the sum fails. Cap-minus-one refuses at the intended preprojection or final syntax boundary, with original current-file diagnostics if that checkpoint has a justified file. Exact cap returns syntax; resolve may need more headroom and must not be claimed passed under that smaller cap. For a combined parser+resolver positive, choose the cap from the measured/hand-counted maximum of **all** phases.
|
||||
5. **Resolver carry:** choose a fixture/cap pair where parsing passes and the carried syntax/catalog/lower/warning aggregate crosses the cap in resolve, while the zero-C version fits. Assert the actual resource refusal and original file/span policy. Separately show a successful full path retains C once, not twice.
|
||||
6. **Text timing/exclusion honesty:** parse a quoted-string/ordinary-position-copy fixture, then require resolve's C.text+T to fail/succeed at its existing text-accounting boundary. Do not report that fixture as rejecting before decoding/copying the syntax Strings. Verify raw Issue exclusion and retained warning admission remain as before.
|
||||
7. **Overflow:** exercise checked C+file metadata, C+U+M, text and resolver initial composition overflow through private test construction and the real call entry. Overflow must return a small safe diagnostic, never wrap/saturate into success or return a partial witness. Distinguish overflow from ordinary cap failure if an earlier C dimension already exceeds the hard ceiling; direct checked-add helper tests may be needed for unreachable-under-valid-policy arithmetic, and must be labeled as such rather than claiming an impossible accepted C.
|
||||
8. **Policy fidelity:** tightened independent bytes/tokens/entries/references/frames/numeric_units and caller model/file/include/depth policies are carried unchanged; a second-snapshot normal-looking input must still obey every tightened limit. Usage is new-snapshot work, not previous Usage+C. No default profile fallback.
|
||||
9. **Failure/ownership lifecycle:** consumed continuation cannot be reused; borrowed bundle/intent remain unchanged; failure exposes only diagnostics. Tests holding a real external baseline allocation prove it remains alive through parse/resolve and drops normally afterward. Future emitter ownership is not simulated as accepted production provenance.
|
||||
|
||||
Existing admission/index small-stack cases remain separate regression tests. This slice promises no new universal stack bound, and preserves the documented environment-sensitive upstream parsing observation.
|
||||
|
||||
## 7. Required explicit clarifications / possible revisions
|
||||
|
||||
**Temporal wording:** parser proposal “bound ordinary temporary containers/references before allocation” and Project contract “before allocating or copying an in-scope representation” must not be cited as proof that this phase-only seam admits every parser allocation before growth. Existing code admits retained syntax text later and has independent parser transient controls. Adoption should explicitly state: this prerequisite carries the surviving baseline through current logical **phase checks**; it neither closes nor waives any stronger before-growth obligations for the final execution contract. If parent interprets the earlier broad wording as an unconditional requirement on every counted parser producer now, that interpretation and this intentionally phase-only deliverable conflict. Parent must explicitly amend/scope the predecessor requirement or defer acceptance of this limited prerequisite; implementation may not silently weaken it.
|
||||
|
||||
**Zero baseline and earlier refusal — selected compatibility rule:** keep the original path and phase checks unchanged, and enforce combined checks only through the explicit carried path, including a legitimate zero C. That carried C=0 path can reject a syntax-only witness earlier where original syntax succeeded but original resolve rejected U+M. Document and test this intended difference. Successful complete outcomes and Usage must agree. A numeric-zero bypass would violate the opt-in carried contract, while globally imposing U+M on legacy syntax would violate the chosen compatibility scope. No claim of identical parser-only acceptance across these two deliberately distinct private paths is made.
|
||||
**Final execution acceptance:** the rename contract requires a surviving aggregate baseline during actual-output parse/resolve and whole-Project equality, but that requirement alone does not approve an emitter or resolve all parser temporal policy. This prerequisite can be accepted independently while the output producer, before-growth decision, E equality reservation, old-owner drop boundary and owning accepted-result design remain unfinished. No release claim of accepted mixed editing follows.
|
||||
|
||||
## 8. Exit criterion
|
||||
|
||||
A reviewed private continuation plus real zero/nonzero-baseline parser/resolver tests establish exact policy retention, disjoint work/live dimensions, aggregate checks at the listed boundaries, once-only resolver transfer and safe failure ownership. Evidence must name the source pin, counted domain, actual refusal phases and remaining timing/exclusion limits. Broader rewritten-output acceptance stays blocked on its own explicit contract and tests, not silently considered satisfied by this prerequisite.
|
||||
|
||||
## Parent scope decision
|
||||
|
||||
Accept this as a limited phase-accounting prerequisite. The existing entry path retains its checkpoints unchanged; only the explicit carried path performs combined checks, even at zero C. This neither satisfies nor waives stronger predecessor before-growth requirements for final output acceptance. The final execution contract must still resolve those requirements before editing authority is enabled. No production entry can yet create a carried continuation: fixture-only construction tests exact-policy capture and propagation, not an implemented consuming old-owner transition. The retained positive fixture counts exactly F map entries, zero lower occurrences, and all actual source/key/entry UTF-8 bytes. Nonzero occurrence or impossible arithmetic cases are explicitly fault injection, never successful owner provenance.
|
||||
@@ -0,0 +1,102 @@
|
||||
# Private retained-entry producer prerequisite
|
||||
|
||||
Baseline `97a8703adc75467a7f8e75f5c6cb00ca542b707c`. This selected contract
|
||||
strengthens the timing of the existing logical entry metric. It does not grant
|
||||
accepted-output, public source8 or save/history authority. Source/evidence remain
|
||||
local until the Gitea storage publication blocker is resolved.
|
||||
|
||||
## Metric and authority
|
||||
|
||||
Add one explicit fixture route enabling both text and entry producers, using the
|
||||
existing opaque CarriedInput and exact inherited policy. Preserve legacy,
|
||||
carried-phase and text-only routes and their diagnostic timing. C comes from the
|
||||
actual carried guard, including an explicitly selected zero guard.
|
||||
|
||||
U remains exactly Budget.usage.entries, the existing cumulative syntax events.
|
||||
M remains F supplied file records + one significant-token-end subtotal + unique
|
||||
ordinary value-end keys + ordinary lexical-owner occurrences + projected slots.
|
||||
Projected slots remain seven per ordinary declaration (including its synthetic
|
||||
body and conservative header envelope), one per field, one per actual Ast value.
|
||||
Node headers also retain the seven envelope. Dropped token/value-end structures
|
||||
keep their conservative subtotal through syntax return. Usage never includes C,
|
||||
M or quota consumption, and significant tokens do not gain a second M charge.
|
||||
|
||||
Before selected growth check checked `C.entries + U + M + delta` against the
|
||||
exact entry ceiling. A failed check changes no U/M total. Use only small scalar
|
||||
optional Budget state; do not duplicate policy or create another owning wrapper.
|
||||
Preserve current byte/guard checks, then atomically validate/install C and M=F
|
||||
before Traversal construction. F is transferred, not charged again, when file
|
||||
records move between containers.
|
||||
|
||||
## Producer and reconciliation rules
|
||||
|
||||
- Budget::entry preserves its existing syntax-work check first; in the new mode
|
||||
check the prospective aggregate before incrementing U or returning to callers.
|
||||
Existing typed/ordinary event sites remain unchanged.
|
||||
- Encapsulate value-end key-presence check, reservation and insertion in one State
|
||||
helper. Charge only a new key. Encapsulate lexical-owner reservation and push
|
||||
likewise; charge occurrences, not distinct UUIDs. Hide unchecked mutable
|
||||
metadata access from the ordinary parser.
|
||||
- Count pending projection with checked scalar grammar hooks: seven per ordinary
|
||||
declaration, one per successfully parsed field and one per successful value.
|
||||
Nested values/fields naturally contribute. Project/include/description do not
|
||||
call these hooks; duplicate fields fail the entire parse. The synthetic body
|
||||
is already in seven, and adds no value event.
|
||||
- At projection boundary atomically reserve token count plus pending projection
|
||||
into M before ends/header/field/value construction. The stronger route must
|
||||
not call the old unmetered borrowed-work Vec counting pass. Other routes keep
|
||||
that existing path.
|
||||
- A scalar prepaid quota is consumed before projector growth: seven before a
|
||||
declaration/header envelope, one before each field, one before each actual
|
||||
value (before recursive children). It neither recharges nor refunds M. Require
|
||||
zero after projection. Under/over quota is an internal invariant failure,
|
||||
never a successful witness with corrected/subtracted counts.
|
||||
- After each file compare reserved map/owner counts with actual lengths and
|
||||
M delta with checked token/owner/map/projected inventory. At syntax completion
|
||||
compare total M with the independently accumulated metadata field. Keep the
|
||||
existing once-only C then U+M resolver ledger transfer; text reconciliation
|
||||
remains unchanged. Never seed a combined total and add syntax a second time.
|
||||
|
||||
## Rejection behavior
|
||||
|
||||
New aggregate ceiling refusal uses `Source8 lowering entry limit exceeded.`
|
||||
Overflow uses `Source8 producer entry size overflow.` Both latch in the new
|
||||
entry mode, leave totals unchanged and propagate immediately. Legacy phase
|
||||
checks keep their existing nonlatching semantics. U-only failure retains its
|
||||
existing precedence and message. Invariant mismatches return an explicit error;
|
||||
they cannot silently repair a count or produce an accepted result.
|
||||
|
||||
Attribute U to its existing event span; owner to the original declaration start;
|
||||
value-end to the completed Ast's start/end; file and complete projection
|
||||
reservation to Span::default in the current file. Grammar counter overflow uses
|
||||
its corresponding declaration/field/value span. Ordinary diagnostic transport
|
||||
continues to use the existing one-byte start representation. Tests distinguish
|
||||
these actual seam diagnostics from later phase or resolver refusal.
|
||||
|
||||
## Acceptance and exclusions
|
||||
|
||||
Real E: C1/U1/M11 gives syntax13/12; full resolver17/16 is a separate boundary.
|
||||
Real three-file chain: C3/U3/M19 gives syntax25/24, separate resolver29/28.
|
||||
Use sufficiently large unchanged text policies (at least E146 and chain358),
|
||||
so earlier text limits cannot preempt the entry assertion. First ordinary owner
|
||||
refuses at prefix cap4; first description value-end refuses at prefix cap7.
|
||||
Neither next integer is claimed to admit the complete document. Review the
|
||||
reserve-plus-mutate helpers and test unchanged collections on refusal directly.
|
||||
|
||||
Independently author a nested record/array/reference/historical UUID fixture and
|
||||
node/element envelopes, with literal counts and spans fixed before runtime.
|
||||
Cover repeated map keys, quota underflow/leftover, overflow and latched refusal
|
||||
as labeled internal controls. Verify actual-owner and explicit C0 routes,
|
||||
whole OA/SA and multi-file Project/Usage/numeric parity against text-only, source
|
||||
conservation, once-only transfer and unchanged old caps/errors. Run new focused
|
||||
cases, full existing disposition, strict core Clippy and format comparison;
|
||||
inspect changed Budget/State/projector prologues and all old small-stack controls.
|
||||
Do not enlarge stacks, loosen tests or add a heap wrapper to hide regressions.
|
||||
|
||||
This retained metric does not bound allocator capacity/node overhead, file-table
|
||||
physical transfer overlap, significant-token storage beyond its existing token
|
||||
bound, traversal/open-declaration/delimiter/Pratt work stacks, duplicate-name
|
||||
scratch, numeric/decoder internals, model/validator heap or native stack. Those
|
||||
named scratch domains need a later explicit finite decision. Actual emitted-byte
|
||||
verification, justified whole-Project equality work and accepted bundle ownership
|
||||
also remain open. No claim of before-every-allocation or all-heap accounting.
|
||||
@@ -0,0 +1,54 @@
|
||||
# Independent critique of retained entry producer prerequisite
|
||||
|
||||
Reviewed ENTRY-PROPOSAL.md against local immutable HEAD `97a8703adc75467a7f8e75f5c6cb00ca542b707c`. No code, compiler, runtime, network or publication action. References below are repository-relative at that source. This is a design review, not implementation acceptance.
|
||||
|
||||
## Disposition
|
||||
|
||||
The proposed scalar grammar count plus prepaid projection quota is a reasonable bounded next prerequisite. It preserves the established logical U/M domain without pretending to bound every allocation. I found no inherent accounting contradiction or need for a parser rewrite. Before implementation, settle the new rejection contract and explicitly distinguish prepaid quota consumption from live accounting. Broader output verification and owner transition remain open.
|
||||
|
||||
## Required precision before implementation
|
||||
|
||||
1. Specify the new aggregate error message, attribution and poison behavior. Existing `Budget::entry` first checks `active`, checks the syntax-only bound, latches a failure, then increments U (`budget.rs:170–206`). Existing metadata phase checks are nonlatching (`mixed.rs:1–17`, `budget.rs:238`). The new mode must run the existing syntax-work check first, then checked aggregate arithmetic, and commit U only after both succeed. Define whether aggregate failure latches, and require immediate propagation consistently; do not inadvertently change legacy error precedence. An overflow must not mutate U/M or masquerade as a limit comparison.
|
||||
|
||||
2. Initialize and validate `C.entries + F` before installing the entry state or constructing Traversal. C comes from the actual guard, including an explicitly present zero guard. The existing source also checks source bytes before the file reservation (`mixed.rs:300–329`): retain that ordering unless the new route's contract intentionally says otherwise. Policy caps in entry tests need generous text/byte/token limits so this earlier validation cannot preempt the claimed entry seam.
|
||||
|
||||
3. Define projection quota as consumption of an already reserved logical subtotal, not another live charge. At the boundary reserve `tokens.len + pending_projection` atomically once; every producer consumes quota before its representation growth, and completion requires zero. Consumption must never reduce Budget M or charge M again. Seven per declaration covers the synthetic body and conservative header envelope, even though node headers have only four actual roles (`language.rs:1121–1168`). No release of M follows temporary ends/value-map destruction: the established metric conservatively retains those subtotals through the witness (`mixed.rs:42–47`, `368–429`).
|
||||
|
||||
4. Make the attribution of new owner/value-map/pending overflow checks explicit. The owner seam occurs after consuming the opening brace (`language.rs:1693–1702`), so a generic current-token error would point into the body, not the declaration start. Pass the selected declaration span deliberately. The value-end seam has the completed Ast offset and end (`1357–1375`); choose and test that source span, including the ordinary one-byte diagnostic transport. The proposal currently promises the original declaration span but does not fully define all new diagnostics.
|
||||
|
||||
## Coverage and lifecycle review
|
||||
|
||||
U is cumulative syntax work, not a count of simultaneously live allocated objects. Keep exactly the old calls: ordinary project/declaration/include/description/field/value events; typed declaration/field/type/dimension/override and expression/operator/call-argument events. Existing `syntax.rs:60–63` excludes grouping from a new entry, and `169–176` charges binary operators before their deferred construction. The new check belongs in the common Budget seam, not additional Vec pushes. Existing Pratt/open-declaration work stacks stay outside this selected metric.
|
||||
|
||||
Successful grammar-derived projection counts match the old walk's shape: seven per ordinary declaration, one per successful field and one per successful value. `language.rs:1294–1343` recursively routes nested object fields through field(), and `1357–1375` routes every child Ast through value(); `mixed.rs:440–465` counts precisely those same retained fields/values. Duplicate fields cause whole-parse failure after insertion (`language.rs:1307`, `1589`) and cannot create a successful replacement that needs a projection refund. Project description and includes bypass field/value and correctly do not contribute projected slots. Incrementing only after successful grammar completion avoids presenting a partial failed parse as a reconciled witness.
|
||||
|
||||
The value-end helper should check key presence, reserve only a new key, then perform insertion in the same private operation. Real successful source generally supplies distinct Ast offsets; a repeated key is a useful labeled internal replacement control, not a claim about ordinary grammar provenance. The owner helper counts declaration occurrences, including repeated IDs, before the Vec push. Final comparisons against actual lengths are needed because merely carrying counters alongside unchecked public fields would not establish producer coverage.
|
||||
|
||||
F is initialized once for every supplied file. The later BTreeMap-to-Vec transfer (`mixed.rs:410–436`) moves those same logical records. It is not another F, and this policy does not claim to bound transient physical container overlap. Similarly, the resolver reconstructs a distinct ledger from C and existing U+M; it must not seed the combined subtotal and add syntax again. Keep existing file/path/model validation and resolver equality checks, rather than inventing a second accepted witness authority.
|
||||
|
||||
## Finite oracle assessment
|
||||
|
||||
The existing independently authored E and three-file fixtures support the proposed totals:
|
||||
|
||||
| Fixture | C | U | M | Syntax total | Separate resolver peak |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: |
|
||||
| E | 1 | 1 | 11 = F1 + token ends10 | 13 | 17 |
|
||||
| Three-file chain | 3 | 3 | 19 = F3 + token ends16 | 25 | 29 |
|
||||
|
||||
Use syntax-only 13/12 and25/24 boundaries, then separate full-resolution17/16 and29/28 boundaries. The text-producer route must have a sufficient actual inherited byte cap; E146 remains the known complete text control. Scanner raw token work31 in the chain must not become M16. Exact chain refusal file/span requires the literal traversal fixture, not aggregate arithmetic alone.
|
||||
|
||||
The owner cap4 control is sound as a prefix: C1 + F1 + project U1 + declaration U1 =4 immediately before the first owner M reservation. The first ordinary description-value cap7 is also a prefix: C1 + F1 + U4 + owner1 =7 before its value-end insertion. Neither cap5 nor cap8 proves a complete successful syntax; the following token/projection subtotal is larger. Timing must be shown by a finite observer or directly reviewed reserve-plus-mutate operation, with a roomy positive instance of the same real fixture. A final error alone cannot prove that no map/Vec insertion happened first.
|
||||
|
||||
For the additional nested fixture, author the literal source and expected U, distinct value offsets, owner count, significant tokens and projection subtotal before writing test expectations. Include both a nested Ast Record and the synthetic declaration body so they cannot be conflated; retain node's seven-envelope distinction. Quota underflow/leftover and arithmetic overflow controls are internal fault injections, not complete-source acceptance evidence. Whole OA/SA/project and Usage parity at ample limits provides the positive semantic control; no generic output-derived counting oracle is needed.
|
||||
|
||||
## Simpler alternatives and boundaries
|
||||
|
||||
Keeping the old borrowed-work Vec count and adding a live scratch allowance would touch fewer grammar sites, but introduces a new peak metric and policy, precisely the expansion this slice avoids. A recursion-based counting pass adds another depth-sensitive walk and is worse after the existing stack investigation. The proposed scalar counter is the simplest option that both eliminates that extra counting frontier on the new path and keeps U/M unchanged.
|
||||
|
||||
Quota could technically be omitted after trusting the grammar count, but then a future projector branch could silently escape the prepaid inventory. A single narrow scalar consumption callback is worthwhile as an internal coverage invariant; do not turn it into a generic visitor or a second retained ledger. Ensure value quota is consumed before recursive children and header envelope before header collection; field quota can precede its recursive value construction. Text and quota callbacks may require a carefully scoped common borrower, not a second Budget copy.
|
||||
|
||||
The existing delimiter Vec at `language.rs:1033–1040`, Pratt stacks, traversal structures and declaration open stack remain named excluded scratch. Significant tokens remain covered by their existing token bound; token-end M is reserved before ends construction, not falsely billed as two N-sized containers. Those exclusions are compatible with a retained-inventory prerequisite but forbid all-heap, physical peak or complete before-allocation claims. Measure actual Budget/State/projector frames and retain all existing small-stack controls without changes; a few scalar fields are not evidence of harmless stack cost.
|
||||
|
||||
Recommended next step: implement only the explicit fixture route, common prospective U check, private metadata helpers and grammar counter/prepaid quota with these finite cases. Retain legacy, carried-phase and text-only behavior verbatim. This produces earlier enforcement of the existing entry metric, not accepted output or closure of broader scratch obligations.
|
||||
|
||||
Parent's subsequent selected clarification resolves the diagnostic ambiguity if adopted explicitly: aggregate refusals reuse the lowering-entry-limit message at the actual producer span and latch only in the new mode; checked overflow has a distinct source-producer-entry-size-overflow error. Nested ordinary declarations do not undermine the successful-count argument: `language.rs:1540–1600,1693–1723` keeps declarations as separate flat occurrences with their own fields and seven-slot envelopes. The open stack supplies lexical ownership, not a second declaration or projected synthetic body.
|
||||
@@ -0,0 +1,59 @@
|
||||
# Existing retained U/M producer timing — proposal
|
||||
|
||||
Source `97a8703adc75467a7f8e75f5c6cb00ca542b707c`; 2026-09-25. Original read-only proposal, selected by the sibling entry-producer contract. Source references are repository-relative at the pinned commit; original local worktree links are transcribed as plain paths for relocation. No build or runtime result is inferred. This is a next prerequisite for the existing private output-verification path, not an accepted-output API or all-heap accounting.
|
||||
|
||||
## Selected-domain invariant
|
||||
|
||||
U remains exactly Budget.usage.entries, the existing cumulative syntax-event charge; do not relabel every allocated object or Vec push as a new U event. M remains exactly the existing conservative subtotal: F complete-file slots + significant-token-end count + unique ordinary value_ends entries + lexical-owner entries + ordinary projection slots. Its dropped value-end/token-end portions remain represented through the syntax witness as before.
|
||||
C is the actual retained guard's immutable Slots, never Usage. In a new explicit entry-producing mode, every selected U/M growth must first check checked `C.entries + U + M + delta <= exact Limits.entries`. A refused reservation installs no new U/M total. Existing occurrence and numeric work semantics do not change; text-producing checks continue independently against the same policy.
|
||||
The final U/M inventory must equal the existing inventory for successful input. This strengthens timing, not the metric. Token storage still uses the token bound: M has ONE token-end component, not separate charges for significant tokens and ends. Capacity, container node overhead and native stack do not become entries.
|
||||
|
||||
## Compatibility and concrete owner
|
||||
|
||||
Keep legacy, carried-phase and text-only routes unchanged, including their phase timing/diagnostics. Add a fixture-only route enabling text plus entry producers from the existing opaque CarriedInput; no arbitrary baseline/profile constructor and no numeric-zero bypass. Existing text-only E146/145 and phase-only tests retain their meanings.
|
||||
A small optional Budget entry state can hold only carried-entry baseline and current M; U is read from existing Usage, never duplicated. Derive C directly from the guard at initialization. Initialize M=F before Traversal::new using the existing file check. No full policy copy, owner enum, new heap container or String wrapper is needed. Measure generated frames after implementation rather than spending the repaired stack margin by assumption.
|
||||
Use Budget::entry as the common prospective-U seam: preserve the old syntax-work check/error behavior, then perform the new aggregate check before incrementing Usage or letting the caller create its associated representation. These checks must not reset work, reinterpret references as occurrences, or add U again at final handoff.
|
||||
A per-file scalar pending_projection count belongs in mixed State, where ordinary grammar callbacks already meet the shared Budget. It counts future projection slots without reserving them prematurely. Keeping this scalar distinct from live M avoids charging output that has not been produced yet.
|
||||
|
||||
## Concrete seams and ownership
|
||||
|
||||
| Existing component | Producer seam | Required action / reconciliation |
|
||||
| --- | --- | --- |
|
||||
| U ordinary events | `crates/oa-core/src/language.rs`:1192 syntax_entry; field1321, value1357, project1513, include1613, description1637, declaration1657 | All route through State::entry/Budget::entry before their current grammar event. Preserve exact event counts and caller order. No charge for every open-stack push or every Rust enum. |
|
||||
| U typed events | `crates/oa-core/src/language/analysis_source/declarations.rs`:261,290,476,542,611; `crates/oa-core/src/language/analysis_source/syntax.rs`:61,169,242 | Existing pre-event calls cover declaration/field/type/dimension/override and Pratt expression/operator/argument events. Grouping mutates an existing expression and does not invent an entry. Deferred node construction uses its already reserved event. Keep the repaired iterative driver unchanged. |
|
||||
| F file records | `crates/oa-core/src/language/analysis_source/mixed.rs`:314–329 | Initial complete-file reservation stays before traversal. Map-to-canonical-Vec consumption transfers the same logical file records; do not add another F merely for the new container. |
|
||||
| Ordinary value-end map | language.rs:1373, after successful Ast construction but before map insertion | A State::remember_value_end helper checks whether the offset is new, reserves one M only for a new key, then inserts. Ast already has its U event. Replacement updates do not add a slot. Per-file reserved count must equal actual map.len before its conservative subtotal is retained/dropped. |
|
||||
| Lexical owners | language.rs:1700, before push and ordinary declaration insertion | A State::push_lexical_owner helper reserves one M before its current Vec push. Count occurrences, not unique IDs. Per-file delta must equal Vec.len. Typed roots/project header do not gain fabricated ordinary-owner entries. |
|
||||
| Token-end metadata | language.rs:1032 ends collection; mixed.rs:368+ preprojection boundary | Reserve exactly tokens.len once before ends creation, alongside projection below. No need to charge M during lex::tokens significant-token push: that producer belongs to the existing token-bound-only domain, and ends does not yet exist. |
|
||||
| Ordinary projected slots | language.rs:1042–1168 Positions and per-declaration construction | Pre-reserve pending_projection once before any projected declaration/header/field/value allocation. Preserve seven logical slots per ordinary declaration/body/header envelope, one per field key, one per Ast-projected value; keep the node-header envelope conservative rather than revising it. |
|
||||
|
||||
Direct map/Vec growth should remain inside the same narrow State helper as its reserve; expose no independent unchecked mutable metadata operation for the new path. The shared ordinary Parser invokes these helpers only through its existing optional mixed State. Public grammar need not acquire a new state owner.
|
||||
|
||||
## Avoid the current counting-work Vec on the new path
|
||||
|
||||
The current `crates/oa-core/src/language/analysis_source/mixed.rs`:440+ is a postparse saturating tally with an unmetered borrowed-work Vec. Calling it before a projection reservation does not establish before-growth accounting for that Vec. Do not silently charge its scratch under the retained projection subtotal or call saturation checked arithmetic.
|
||||
Recommended replacement ONLY for the new route: increment checked pending_projection by7 when an ordinary declaration is created, by1 for every successful ordinary field(), and by1 for every successful ordinary value(). Nested objects/arrays are visited naturally by those existing calls; each child value and nested key contributes exactly once. Project/include/description grammar does not call field/value in that path and must not enter this projection counter. Failed whole parses need no successful-witness reconciliation.
|
||||
At the projection boundary, reserve pending_projection + tokens.len into M once. Maintain a scalar projection quota consumed by the actual projected producer:7 per declaration,1 per field,1 per EditValue. Check quota subtraction before the corresponding production and require zero at completion. This compares the independent grammar-derived inventory with actual projection consumption without a second AST walk or an extra collection. Static header roles are included in the existing7 envelope; do not also subtract their individual count.
|
||||
Legacy/carried-phase/text-only retain ordinary_position_slots and their prior behavior. The stronger path's quota hook can be an additional narrow optional callback/scalar in the existing shared projector; do not add a generic visitor framework or clone ASTs. The source review must compare grammar counting order and projection consumption coverage, including synthetic body Record handling.
|
||||
The synthetic per-declaration body Record is already part of7 and is NOT a second value() contribution. Ast::Object below a field is an actual value and consumes1 plus its field entries; Ast::Array consumes1 plus items. Atom/Reference/HistoricalUuid each consume1. This distinction needs explicit tests.
|
||||
|
||||
## Transfer and final checks
|
||||
|
||||
After each file, compare its reserved value-end/owner counts with actual lengths and its consumed projection quota with zero. Compute the expected M increment from those lengths, tokens.len and the reconciled projection count; compare against the producer M delta instead of adding it again. Initial F is checked separately once.
|
||||
At final syntax return assert Budget's M equals ParsedMixedSyntax.metadata and the file inventory's checked total. Keep Usage.entries as the original U. The existing resolver still seeds C once and reserves U+M once in its distinct ledger; this is transfer/reconstruction, not an additional simultaneously retained allocation. Do not seed C+U+M and then call the unchanged additive syntax path again. Text T transfer/reconciliation remains as implemented.
|
||||
No new occurrence storage is allocated in this slice; C.occurrences continues its inherited checks. If a future producer introduces an auxiliary container, it is not covered by these retained counters unless explicitly inventoried.
|
||||
|
||||
## Finite acceptance oracles and limits
|
||||
|
||||
1. Empty E: C.entries1, U1, M11 (F1 + ten token ends), final syntax13; cap13 parses,12 refuses before token-end/projection creation, full resolver peak17 remains distinct. Real inherited text policy must be sufficiently large (producer E146) so text cannot preempt the entry control.
|
||||
2. Existing three-file chain: C.entries3, U3, M19 (F3 + sixteen token ends), final25; syntax25/24 and resolver29/28 remain separate cases. Verify actual file/checkpoint attribution and no double-add at transfer. Scanner raw-token work is31, not M's sixteen significant tokens.
|
||||
3. Minimal ordinary empty activity: before its first lexical-owner push C1+F1+U2=4; entry cap4 can distinguish the new owner reservation refusal. It is a negative timing control, NOT a claim cap5 admits the complete syntax. Use actual guard ownership, generous text/token limits and original declaration span.
|
||||
4. First ordinary `description ""` value: C1+F1+U4+one lexical owner=7 immediately before its new value-end entry; cap7 refuses before map insertion. Again cap8 only fits that prefix, not the later complete metadata. A bounded test observer or separately reviewed reserve-plus-insert helper must establish ordering; a final retained count alone cannot.
|
||||
5. Independently hand-count flat fields, nested record/array/reference/HistoricalUuid and node versus element headers: exact grammar projection counter equals old retained inventory, and actual projection quota ends at0. Invalid under/over quota and repeated map-key controls are labeled internal fault injection, not real source provenance. No AST cloning or output-derived expected model.
|
||||
6. Whole real OA/SA/multi-file outcomes and Usage/numeric parity against text-only at ample limits; explicit C0 new mode; old route cap/error behavior unchanged. Work Usage counts must not absorb C, M or projected quota events.
|
||||
7. Preserve all old 128 KiB/deep/wide controls and measure changed Budget/State/projector frames. No stack/test-size relaxation or RUST_MIN_STACK. Repeated failure must not leak a partial witness or new source authority.
|
||||
|
||||
## Finite domain decision still required
|
||||
|
||||
The projector also uses a delimiter Vec at language.rs:1033–1040; traversal and ordinary open-declaration coordination likewise remain existing scratch, not retained M. The proposed contract can honestly cover U/M while leaving those named scratch producers open. It cannot claim the single N token-end reservation accounts two simultaneous N-sized containers. If the parent requires these scratch entries now, choose an explicit additional live W, reserve before each push/release on pop, and derive new peak oracles; that is a separate finite metric addition.
|
||||
Comparison: keep the old count walk and add bounded W for its work frontier (smaller initial grammar edits, but expands scratch policy and peak caps), versus grammar scalar + producer quota (more named seams, no new frontier/domain). Recommend the latter for this narrowly retained-inventory prerequisite. Broader scratch, final actual-output equality and accepted-owner transition remain out of scope. No additional task is proposed or started here.
|
||||
@@ -0,0 +1,151 @@
|
||||
# Private actual-output rename acceptance
|
||||
|
||||
Baseline `29417130c71f70c4eaefe8ed4c879efba78eb72c`. Select the complete pending
|
||||
owner borrow, actual-output equality bound and moved model/warning outcome after
|
||||
independent design, domain and ownership critiques. This connects the verified
|
||||
emitter and named parser producers; it is no longer another fixture-only
|
||||
continuation. The surrounding mixed implementation remains private experimental
|
||||
source. Public source8, installation/history and release integration are separate.
|
||||
|
||||
## Authority and result
|
||||
|
||||
Add a consuming verification operation on opaque PendingRename. Its only normal
|
||||
constructor remains validated consumed emission. No production constructor may
|
||||
accept an arbitrary bundle/model/Slots/profile tuple. Add CarriedInput::from_pending
|
||||
accepting a shared reference to that complete pending owner; derive bundle,
|
||||
exact Limits/ProjectLimits and retained O from the same owner. Both backing and
|
||||
guard retain a complete pending reference, not just numeric counters. Keep enum
|
||||
variants private and expose only narrow immutable pending views to carried code.
|
||||
|
||||
Prefer `&'s PendingRename<'s>` inside the existing single-lifetime backing, using
|
||||
the immutable desired reference's covariance to shorten a longer desired borrow
|
||||
to the verification borrow. Actual Rust compilation must establish that relation.
|
||||
Do not thread a second lifetime through every parser owner, duplicate full policy
|
||||
inside guards, introduce a heap wrapper or permit mutation while the guard lives.
|
||||
The existing fixture constructors and old parser entry points keep their behavior.
|
||||
The new continuation is the sole emitter-connected entry into the combined text
|
||||
and retained-entry producer route, including actual zero metadata if applicable.
|
||||
|
||||
On success return a non-self-referential owning AcceptedRename with the actual
|
||||
SourceBundle, actual resolved Project, mapped warnings, exact inherited policies
|
||||
and final retained Slots. Preserve scalar verification Usage and numeric work
|
||||
separately from original-snapshot work; no cumulative reinterpretation. The
|
||||
accepted type has private fields and read-only accessors, with no unchecked or
|
||||
mutable constructor, borrowed syntax or borrowed desired model. Source, desired
|
||||
and pending may be dropped before the accepted result is inspected.
|
||||
|
||||
This result does not itself save files, edit history, install a model or enable
|
||||
repeated carried-source emission. Later adoption must jointly preserve its
|
||||
graph/source/diagnostic relationship; no separate mutable extraction is added.
|
||||
No-op remains the existing explicit unchanged-owner branch.
|
||||
|
||||
## Actual verification and equality
|
||||
|
||||
Parse the actual complete pending bundle under its exact policy using both named
|
||||
producer modes. Propagate its located parse diagnostics. Consume the resulting
|
||||
syntax through the existing resolver and complete model admission, preserving O
|
||||
through all parser/resolver stages. Never default policy, substitute intended
|
||||
tokens/model, drop untouched files, re-emit bytes for comparison or reset carry.
|
||||
|
||||
After actual model admission reserve `E_actual = resolved.expressions.len()`
|
||||
entry slots against that resolved owner's full live ledger, which includes O,
|
||||
syntax and mapped warnings. Compare the entire actual Project with desired using
|
||||
its established PartialEq, including aliases, parameter names, all maps and
|
||||
historical payloads. Do not use rename::same_project, which exempts names. Release
|
||||
the equality frontier after comparison. A mismatch returns a small static
|
||||
invalid_source diagnostic at entry/default span0..0, not a formatted model.
|
||||
|
||||
The independently reviewed bound follows every reachable actual Expr root:
|
||||
calculation body, property base, case overrides and check predicate. Each actual
|
||||
node has a source expression-site entry. Iterative Expr equality's live pairs
|
||||
correspond to distinct nodes of its actual left operand; list/call shape checks
|
||||
precede child queueing, and no nested equality frontier remains live. Whole-model
|
||||
comparisons visit roots sequentially. E_actual0 cannot invoke an allocating Expr
|
||||
comparison from a nonexistent actual root. Existing signed-zero PartialEq remains
|
||||
semantic equality, not bitwise equality. Revisit the proof when payload types or
|
||||
lowering/equality implementations change. Original pending E is provenance only;
|
||||
the new implementation must use the current resolved arena for its reservation.
|
||||
|
||||
## Mapped warnings and ownership transfer
|
||||
|
||||
Retain the mapped warnings by moving them, together with the actual resolved
|
||||
Project, after successful equality. The model remains in its existing excluded
|
||||
model-allocation domain and has already passed admission. Mapped warnings remain
|
||||
in the selected auxiliary domain. Do not clone a model, warning, path or vector.
|
||||
|
||||
Compute warning inventory W with a checked borrowed loop over the actual records:
|
||||
one entry per warning, zero occurrences, and UTF-8 lengths of path, code, optional
|
||||
field and message. Require current mapper invariants: every related vector empty
|
||||
and no Error severity. Unexpected shapes fail before producing accepted authority;
|
||||
do not silently ignore newly introduced fields/related locations. The diagnostic
|
||||
field accounting should be exhaustive enough that schema changes demand review.
|
||||
|
||||
W already participates in the full resolver ledger. Do not charge it twice during
|
||||
equality or transfer. Compute final O+W with checked arithmetic, verify it is a
|
||||
componentwise subset of the still-live ledger, and check its exact inherited
|
||||
ceilings while Budget exists. This is a destination-inventory check, not an early
|
||||
release: keep full coexistence accounting until all temporary owners actually
|
||||
drop. Move Project/warnings to locals, drop all verification syntax/guard,
|
||||
expression/occurrence/catalog owners, then move the pending bundle/policy into
|
||||
AcceptedRename. Rust must enforce that no pending borrow survives that move.
|
||||
Final retained counts exclude released syntax and equality scratch but include
|
||||
all surviving mapped warnings. Failure returns only diagnostics and destroys
|
||||
the consumed pending output and partial verification state.
|
||||
|
||||
## Explicit remaining scratch decision
|
||||
|
||||
Choose the existing logical retained domain plus the named decode/equality
|
||||
temporaries; do not expand it into universal parser/all-heap accounting. Existing
|
||||
traversal, delimiter, ordinary-open, Pratt and duplicate-name scratch retain their
|
||||
independent file/edge/token/grammar controls and are excluded from carried W.
|
||||
Significant-token storage retains its independent token ceiling; M counts the
|
||||
separate token-end representation once. No new output-verification collection
|
||||
may be hidden under an old exclusion.
|
||||
|
||||
File count/source-text limits do not tightly bound copied traversal path bytes.
|
||||
Path folding/canonicalization and include target/location copies can occur before
|
||||
edge refusal; retain that known limitation explicitly. Open-declaration depth is
|
||||
bounded by declaration events, not typed-value depth. Pratt frames are not native
|
||||
stack bytes. Decoder/numeric internals, raw model/validator allocation, allocator
|
||||
capacity/overhead and native stack remain their established separate domains.
|
||||
Do not claim aggregate admission before every allocation or process-memory bounds.
|
||||
This resolves the earlier scratch-domain choice without waiving selected retained
|
||||
producer timing, actual output admission, equality or returned ownership.
|
||||
|
||||
## Finite acceptance
|
||||
|
||||
Use unchanged inherited profiles from original parse/index/plan through emission
|
||||
and verification. Never tighten an admitted owner to manufacture a late failure.
|
||||
Minimal project p→next emits81 bytes, O=(1,0,105), T11. Entries17 and bytes152
|
||||
admit the full pipeline; bytes151 reaches output UUID decode at37..38, line2
|
||||
column27, after scanner peak141 and emission peak114. Entry16 fails emission
|
||||
first and is not a later verifier cap. Assert the entire independent expected
|
||||
Project/bundle, empty warnings, final O, exact policy and separate snapshot Usage.
|
||||
|
||||
Retain a real warning-bearing rename with an independently authored full
|
||||
diagnostic and exact final O+W. Distinguish final retention counts from greater
|
||||
earlier peaks; do not invent a final-only cap-minus-one boundary. Reuse emitter
|
||||
OA/SA/cross-file/Unicode/CRLF/swap byte oracles and whole expected models where
|
||||
valid, checking returned warning paths/spans against the actual returned bytes.
|
||||
The [empty-name warning oracle](analysis-mixed-acceptance-design-20260925/WARNING-ORACLE.md)
|
||||
uses unchanged entries18 and bytes161/160: old emission peaks159 bytes, actual
|
||||
output warning mapping161, and accepted O+W=(2 entries,0 occurrences,157 text).
|
||||
Its negative warning-map diagnostic retains the full project-header11..69 span,
|
||||
unlike the parser's one-byte token-start transport. These are expectations to
|
||||
verify, not results inferred from source arithmetic.
|
||||
|
||||
Equal-byte test-only corruption can cover wrong-but-admissible alias/name,
|
||||
malformed source and model-invalid output while preserving O. A length-changing
|
||||
corruption must first be rejected by an explicitly selected inventory check or
|
||||
use independently verified actual-O fixture provenance, never stale counts.
|
||||
Include greater actual expression count than old E, wide/deep comparison,
|
||||
target-only mismatch and signed-zero cases. Label internal inventory/shape faults
|
||||
as such; do not relabel them as authentic emitted histories. Check true owning
|
||||
lifetime by returning success out of a scope that drops original input/desired.
|
||||
|
||||
Preserve all previous tests, failure bodies and format disposition; run focused
|
||||
new cases, full core library, strict all-target Clippy and format comparison.
|
||||
Exercise meaningful scoped128KiB verify/drop cases with prerequisites prepared
|
||||
outside the worker, preserve all old stack controls and inspect changed
|
||||
prologues. Do not enlarge stacks, loosen policies or rerun failures until lucky.
|
||||
Older separately unrerun full-pipeline setup limitations remain distinct.
|
||||
@@ -0,0 +1,136 @@
|
||||
# Independent actual-output acceptance oracle — proposal only
|
||||
|
||||
Source inspected: `a629ae01d49bb3cb482a047eb41832ad4246a5c6`. Derived from literal
|
||||
bytes and existing source, not parser/emitter execution. Only string lengths and
|
||||
literal substring offsets were evaluated. Future verification API/domain/equality
|
||||
contract is not yet selected; all runtime acceptance remains pending.
|
||||
|
||||
## Minimal real rename, one unchanged inherited policy
|
||||
|
||||
Entry/key `main.arcadia` has 12 ASCII bytes. Original exact source (final LF):
|
||||
|
||||
```text
|
||||
arcadia 8;
|
||||
project p "Project" id "00000000-0000-0000-0000-000000000001" {
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
Desired differs only in project alias `p` -> `next`. Expected complete emitted
|
||||
bundle is exactly one file under the same entry/key, containing (final LF):
|
||||
|
||||
```text
|
||||
arcadia 8;
|
||||
project next "Project" id "00000000-0000-0000-0000-000000000001" {
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
Original source is 78 bytes; output is 81. The sole edit is original [19,20),
|
||||
replacement `next` (four bytes); output alias is [19,23), display text [25,32),
|
||||
UUID quoted token [37,75). Output retained O=(1 entry, 0 occurrences, 105 text),
|
||||
where 105=81 source+12 map key+12 entry bytes. No output key/path is omitted.
|
||||
|
||||
Complete expected Project: schema_version=8; id=00000000-0000-0000-0000-000000000001;
|
||||
alias="next"; name="Project"; description=""; definitions, elements, relations,
|
||||
diagrams, artifact_links and traces are all empty maps. Expected warnings empty.
|
||||
This asserts every model field, not just the changed alias. E_original=E_actual=0.
|
||||
|
||||
Select original Limits entries=17, bytes=152 (or 151 negative), all other fields
|
||||
unchanged from an admissible roomy source8 profile; keep the same ProjectLimits
|
||||
through original parse/index/plan/emission/verification. Never tighten live policy.
|
||||
|
||||
| Stage | Logical entries | Logical text | Derivation |
|
||||
| --- | ---: | ---: | --- |
|
||||
| Original syntax retained |12|8|U1 + M11; alias1+name7 |
|
||||
| Original resolver peak |16|10|Project catalog +4 entries, two alias copies |
|
||||
| Original typed index retained |13|9|One global name; no typed headers/expressions |
|
||||
| Original ordinary index retained |14|9|One authored project header |
|
||||
| Rename plan peak/retained |16|9|R1 name target + namespace Q1 (then dropped), M1 edit |
|
||||
| Emission overlap |17|114|Plan16/9 + output O1/105 |
|
||||
| Output strong syntax retained |13|116|C1 + U1 + M11; C105 + T11 |
|
||||
| Output resolver peak |17|124|Syntax13 + project catalog4; two four-byte alias copies |
|
||||
| Output resolved retained/equality |13|116|Catalog dropped; E_actual0 equality scratch |
|
||||
| Owning accepted bundle after verification owners drop |1|105|Only actual output O remains |
|
||||
|
||||
Original indexing/transient peaks do not exceed the original resolver/plan/emission
|
||||
entry ceiling17 for this empty model; prior emitted-overlap source tests explicitly
|
||||
use entries17 and bytes114 through the real predecessor path. The new proposed
|
||||
152/151 byte profiles are looser than that already authored predecessor boundary.
|
||||
No new verification allocation occurs while the old authored owner remains live.
|
||||
|
||||
## Exact new byte boundary and timing
|
||||
|
||||
Scanner validation of the output UUID reserves raw payload36 before decoding.
|
||||
No grammar text exists during that file scan: C105+W36=141 (display payload7 is
|
||||
smaller). Grammar first retains alias4 and display name7, hence T11. Its UUID
|
||||
consumer reserves P36 while T11 remains live: 105+11+36=152, before the existing
|
||||
serde decode. Valid decode D36 transfers then UUID parsing drops/refunds it;
|
||||
final T11 and W0. Project has no projected ordinary values or textual keys.
|
||||
|
||||
Therefore 152 is the full-pipeline positive byte cap and 151 isolates the new
|
||||
actual-output grammar producer refusal, after predecessors/emission and scanner
|
||||
have fit. Expected source path main.arcadia, code invalid_source, message
|
||||
`Source8 index/output byte limit exceeded.`, ordinary transported span [37,38),
|
||||
line2 column27, no related locations. Raw quoted extent is [37,75), but the
|
||||
ordinary error transport intentionally retains its one-byte start representation.
|
||||
Do not expect this refusal at the former unverified-emission114 boundary.
|
||||
|
||||
Entry cap17 permits the whole pipeline, but cap16 refuses **emission first**:
|
||||
plan16 plus O1 cannot fit. Thus it cannot demonstrate a later verification/resolver
|
||||
entry refusal. Equality reserves zero here; no equality-specific minus-one case
|
||||
exists. A different justified fixture is needed for any proposed late entry gate.
|
||||
|
||||
## Equal-byte corruption controls (not genuine emitted results)
|
||||
|
||||
Start each control from a fresh legitimately emitted pending owner under roomy
|
||||
unchanged inherited limits. Mutate only in the private fixture harness. Equal UTF-8
|
||||
length preserves O numerically; production must not expose mutation/constructors.
|
||||
|
||||
- Output alias [19,23): `next` -> `nope`. Valid alias and otherwise empty model;
|
||||
parse/admission should pass, complete Project equality must reject.
|
||||
- Output name [25,32): `Project` -> `Changed`. Valid decoded display name, same
|
||||
seven bytes; parse/admission should pass, non-name equality must reject.
|
||||
- Version byte [8,9): `8` -> `?`. Equal bytes, malformed source; scanner/parser
|
||||
must reject before model equality. Do not claim a successful wrong-model case.
|
||||
|
||||
Each failure must drop pending verification state, return no accepted owner, and
|
||||
leave original source and desired unchanged. Do not recursively format models.
|
||||
The new verifier's semantic-mismatch message is deliberately unspecified here.
|
||||
|
||||
## Actual expression count larger than original, unchanged O
|
||||
|
||||
Use the same project wrapper with this exact single body line:
|
||||
|
||||
```text
|
||||
calculation c "C" id "00000000-0000-0000-0000-000000000002" { result Number; body 10000; }
|
||||
```
|
||||
|
||||
Original source168 bytes; a genuine project p->next rename emits171 bytes, O=(1,0,195).
|
||||
Desired includes the unchanged Number calculation c, same ID/name, no parameters,
|
||||
and literal10000 expression. Original arena E=1. On a fresh pending corruption
|
||||
fixture replace output expression [160,165) `10000` with `1 + 2` (five bytes).
|
||||
The bytes still describe a Number calculation, now binary Add with two numeric
|
||||
children: actual raw-expression arena E_actual=3. Bundle lengths/keys/entry/O
|
||||
remain unchanged; complete equality must reject its different body. Use roomy
|
||||
entries/bytes4096 from the original parse, not a policy replacement after emission.
|
||||
|
||||
The equality reservation must derive 3 from the freshly resolved actual output,
|
||||
not trust pending's old scalar1. This fixture proves provenance/count selection;
|
||||
its unequal root variants may short-circuit comparison and do not prove a frontier
|
||||
actually exceeding1. Lowering already reserves five entries per actual node,
|
||||
while equality requests only E_actual. No isolated equality-cap-minus-one refusal
|
||||
is claimed; the earlier lowering peak may dominate. A meaningful additional
|
||||
wide/deep positive and target-shape mismatch must be justified independently.
|
||||
|
||||
## Source basis and limits
|
||||
|
||||
At the pinned source, `rename/emission.rs:6` defines the sealed pending fields;
|
||||
its output_slots counts all source/key/entry bytes, and emit_changed_unverified
|
||||
reserves old plan+O before copies and drops old source before constructing pending.
|
||||
`rename/emission/tests.rs` already pins literal NEXT and predecessor17/114.
|
||||
`mixed/project.rs:245`/`:315` reserve the project alias/owner/table catalog; catalog
|
||||
storage releases before returning resolved state. `mixed/project/accounting.rs`
|
||||
seeds syntax and lower scratch independently; LowerStorage::all uses5N+2A.
|
||||
`budget.rs::quoted_text` and raw_tokens StringEvent implement the P/D timing above.
|
||||
These are logical storage derivations, not allocator/RSS or runtime test claims.
|
||||
@@ -0,0 +1,85 @@
|
||||
# Actual emitted-output acceptance — working proposal
|
||||
|
||||
2026-09-25, parent read-only design based on source97a8703 and the entry-producer
|
||||
contract; entry implementation has not yet passed gates. This is neither selected
|
||||
contract nor permission to activate public source8. No next implementation claim.
|
||||
|
||||
The useful next outcome is a single owning accepted output after parsing the
|
||||
actual complete emitted bytes with inherited policy and carried output storage,
|
||||
resolving/admitting them and comparing the entire Project to desired intent.
|
||||
Do not insert another unneeded fixture-only milestone after the retained producer
|
||||
requirements pass. Remaining scratch-domain precision still needs explicit
|
||||
decision and independent critique; see DOMAIN-DECISION-CRITIQUE.md when ready.
|
||||
|
||||
## Narrow authority/lifetime candidate
|
||||
|
||||
Current private emission::PendingRename contains actual SourceBundle, exact
|
||||
Limits/ProjectLimits, reserved output Slots, original E and immutable desired
|
||||
Project borrow. Its only non-test constructor is consumed emission. Existing
|
||||
carried::CarriedInput currently accepts borrowed or fixture RetainedSnapshot.
|
||||
|
||||
Candidate A: add a named CarriedInput::from_pending(&PendingRename), with access
|
||||
only through read-only sealed pending views. It must derive all bundle, exact
|
||||
policy and retained slots from that same pending owner, accepting no loose
|
||||
bundle/profile/Slots. Backing can store the existing bundle/policy plus a shared
|
||||
reference to the pending retained Slots; guard needs only that reference after
|
||||
policy transfers to Budget. The function's borrow lifetime keeps the entire
|
||||
pending owner unavailable for mutation or consumption during verification. This
|
||||
avoids introducing desired's second lifetime into every parser owner. Prove this
|
||||
with actual Rust type checks; do not assume a reference to counters alone is
|
||||
sufficient authority if construction can be separated from the owner.
|
||||
|
||||
Candidate B: move owned snapshot storage into a shared RetainedSnapshot wrapper
|
||||
used by PendingRename, then reference it directly. This may simplify backing but
|
||||
must avoid an unrestricted new(bundle,slots,policy) constructor or unnecessarily
|
||||
coupling parser construction to emitter internals. Compare actual code size and
|
||||
representation before choosing. No generic trait/callback allocator framework.
|
||||
|
||||
A consuming PendingRename::verify method borrows itself for the strong combined
|
||||
text/entry parse; every result remains local until success. Consume syntax through
|
||||
project::resolve under the same carried guard, so complete model admission is
|
||||
the existing one. After equality and bounded validation, drop resolved syntax,
|
||||
model/catalogs and guards explicitly, then move SourceBundle into a narrow
|
||||
owning accepted result. Failure returns located diagnostics and drops pending
|
||||
output; no usable partial bundle or accepted token escapes. An owned accepted
|
||||
bundle is not a self-referential parsed witness. Repeated retained-source editing
|
||||
and actual filesystem/history installation remain distinct work.
|
||||
|
||||
## Whole equality and scratch
|
||||
|
||||
Reuse complete Project PartialEq, including established signed-zero semantics;
|
||||
do not reuse rename::same_project, which intentionally exempts names. The
|
||||
comparator's left operand must be the freshly resolved actual output model.
|
||||
Use that resolver's actual raw-expression-site count E_actual as the conservative
|
||||
equality frontier bound, reserving it against its live ledger before comparison
|
||||
and releasing after equality returns. Original E remains provenance only and
|
||||
cannot bound faulty emitted output. Existing planner proof covers Expr's iterative
|
||||
frontier only when every actual left-model Expr root is represented in the source
|
||||
expression arena; re-audit this reachability for full Project equality (including
|
||||
opaque historical carriers) before selecting E_actual. Original scalar count is
|
||||
not proof of actual output topology. No additional counting traversal or cloning
|
||||
desired model should be introduced without its own bound.
|
||||
|
||||
Reject malformed bytes, model-invalid bytes, admissible-but-wrong aliases and
|
||||
admissible-but-wrong non-name semantics. Error reporting must not recursively
|
||||
format arbitrary models. Exact input/output policies survive; new parser Usage
|
||||
is separate snapshot work. Output C remains live through syntax, resolver and
|
||||
equality and must never be reset to zero or added twice.
|
||||
|
||||
## Required finite acceptance before implementation
|
||||
|
||||
Reuse published emitter exact OA/SA/all-file/Unicode/CRLF maps as independent
|
||||
byte expectations. Construct at least one actual emitted owner whose inherited
|
||||
profile admits original parse/index/plan/emission and then passes verification,
|
||||
plus cap-minus-one refusal at the intended later stage; do not tighten a live
|
||||
owner after admission. Fake/mutated pending bytes are labeled corruption controls.
|
||||
Original empty/project rename may have a new combined producer peak greater
|
||||
than the emitter's former114-byte oracle: recompute the whole pipeline honestly.
|
||||
|
||||
Test wrong-but-admissible output, malformed output, exact policies, complete
|
||||
source/target immutability and scope/lifetime of returned ownership. Wide/deep
|
||||
actual output versus target-only shape mismatch must establish equality's chosen
|
||||
frontier bound. New tests do not replace existing public/legacy/fmt dispositions,
|
||||
stack controls or the separately unrerun older full-pipeline setup limitation.
|
||||
Full parsed-output acceptance remains private until a separate public integration
|
||||
contract reconciles all16 schema8 failures and complete graphical/source behavior.
|
||||
@@ -0,0 +1,29 @@
|
||||
# Independent actual-output acceptance critique
|
||||
|
||||
2026-09-25. Design-only readback of ACCEPTANCE-PROPOSAL.md SHA256 `b59ca190e3cde85611158542ccf86e67c210b853b09a1fd0354daf57d4304509`, the ownership source map and domain critique, against local source `29417130c71f70c4eaefe8ed4c879efba78eb72c`. Entry runtime acceptance remains separate. No source edits, compiler, parser or network execution.
|
||||
|
||||
The proposed actual-byte parse/resolve/full-equality boundary is the useful next outcome. No further fixture-only intermediary is intrinsically required once the selected prerequisites pass. Prefer Candidate A as refined by OWNERSHIP-SOURCE-MAP: retain a complete immutable PendingRename reference, with lifetime shortening through its covariant desired Project borrow. This is simpler and stronger to inspect than projecting authority down to a bare Slots reference, and avoids Candidate B's unnecessary rearrangement of existing owning fields. Actual type checks must establish the proposed coercion and borrow scope; this review does not claim compilation.
|
||||
|
||||
## Ownership and accounting decisions to retain
|
||||
|
||||
Pending's only production constructor is consumed emission (`mixed/project/ordinary_index/rename/emission.rs:6` and its concluding construction). Its bundle, policy and retained Slots are private; emission reserves complete O before copies and drops the old indexed owner before Pending escapes. A from_pending constructor must accept the complete pending reference only and derive all views from that object. Keep guard variants private, add no arbitrary production tuple constructor, and do not copy the full policy into the guard. Existing output keys, entry and all files remain C through the new parse and resolver.
|
||||
|
||||
Put verification in a consuming Pending method. A local scope can borrow pending, use the strong text+entry route, consume syntax through existing project::resolve, reserve equality scratch on that resolved owner's live ledger, compare, then drop the resolved owner and all borrowing guards. Only afterward move the actual SourceBundle into a non-self-referential accepted owner. Returning borrowed syntax alongside it, resetting C to zero, defaulting policies or exposing a mutable Pending bundle would defeat this seam. None is needed. No-op remains the separately consuming original-owner branch.
|
||||
|
||||
Choose the accepted result's retained contents explicitly. If only bundle/policy/accepted authority survives, drop resolver warnings with verification storage. If warnings are retained or returned alongside it, their mapped strings/path copies and destination slots survive too and need an explicit transfer; final live storage is no longer O alone. `mixed/project.rs` currently reserves every mapped warning before pushing it (around lines 588–643). Do not silently clone warnings or equate excluded raw validator Issues with those counted mapped diagnostics. A small entry/default-span mismatch diagnostic is sufficient; recursive formatting of whole models is unnecessary.
|
||||
|
||||
## Actual-expression equality bound
|
||||
|
||||
Use `resolved_actual.project().eq(desired)` under reserved `E_actual = resolved_actual.expressions.len()`, not the old pending scalar or rename::same_project. All current Project-to-Expr roots remain CalculationDefinition.body (`analysis/types.rs:85–94`), EngineeringProperty.base, AnalysisCase.overrides and AnalysisCheck.predicate (`model.rs:667–682`). Historical Trace/SemanticBaseline/FacetReview payloads are closed and contain ordinary strings/IDs/LifelineBinding rather than analysis Expr (`semantic/types.rs:84–174`, `model.rs:526,1632–1650`). The model/type/equality/lowering files are unchanged from the previously reviewed `18073a6` proof.
|
||||
|
||||
`lower.rs:687–699,755–846` routes each real binding through expression lowering, emits one ExpressionSite per visited node, and schedules all child variants; mixed resolution retains this arena while merging model definitions/elements. `analysis/expression_lifecycle.rs:170–270` keeps one iterative pair frontier, checks list/call shape before child insertion and never invokes another Expr equality with its frontier live. Pair members correspond to distinct nodes of the actual left tree, so every simultaneously live frontier is bounded by its actual tree size, hence E_actual. Sequential whole-Project field/container comparisons do not require summing all separate equality Vecs. E_actual may conservatively reject a comparison that would short-circuit; do not promise unconditional success with no phase headroom.
|
||||
|
||||
Make E_actual=0 explicit: derived Binding/parent variant checks reject target-only Expression before entering Expr::eq; no allocating equality call has a zero-node actual Expr. Keep whole-model PartialEq, including +0/-0 equivalence across geometry, ordinary and historical real carriers. Equality establishes semantic PartialEq, not bitwise target identity. Renew this reachability proof if Project payload types or expression production change.
|
||||
|
||||
## Corruption controls and finite gates
|
||||
|
||||
Corrupted Pending tests must preserve their claimed live-storage provenance. Equal-byte replacements can change valid semantics, syntax or expression topology while retaining the authenticated O. Length-changing test mutations must either be rejected by an explicitly selected inventory invariant or construct an independently verified actual-O fixture under the inherited policy; keeping stale O after growing the bundle is not evidence of correct carried accounting. Especially test faulty actual output with more expression nodes than original E to demonstrate why E_actual is used.
|
||||
|
||||
Use one unchanged inherited profile for original parse/index/plan/emission and verification. Recompute the combined original and new-snapshot peaks; a syntax-only cap need not admit the independent counting walk, resolver catalogs or final equality. Require a real full-pipeline positive and a cap-minus-one that genuinely reaches the intended late stage, plus wrong-but-admissible names and non-name data, malformed/model-invalid output, E=0 target-only damage, actual wide/deep equality and signed zero. Drop original inputs and desired after success and inspect the independently owning returned bytes. Failed verification must yield only diagnostics and drop Pending, without exposing a usable partial bundle.
|
||||
|
||||
The domain critique's explicit independent bounds for named remaining scratch are compatible with this outcome; they must be selected and documented, not described as aggregate carried reservations. Existing bounds/exclusions do not exempt new output ownership, equality frontier or retained mapped diagnostics. Native verifier/drop frames require concrete measurements and existing stack controls; earlier separately unrerun setup failures are not converted to success by this design. Public source8, repeated retained editing and filesystem/history adoption remain separate contracts.
|
||||
@@ -0,0 +1,55 @@
|
||||
# Accepted owner contents: independent critique
|
||||
|
||||
Read-only inspection of the current resolver/warning mapper and owned diagnostic schemas, with unchanged relevant behavior from97a8703 through2941713. No source modification, build or runtime claim. This recommends a contract choice; parent owns selection.
|
||||
|
||||
## Recommendation
|
||||
|
||||
Prefer an opaque owning result containing the verified SourceBundle, the **actual freshly resolved Project**, mapped warnings, exact inherited policy and its final retained Slots. This avoids throwing away already admitted graph data and actionable diagnostics immediately before a later coherent source/graph adoption step. It remains a narrow non-self-referential value: Project and SourceDiagnostic own their contents and do not borrow syntax. No history/install operation, public source8 activation or repeated-edit constructor follows automatically.
|
||||
|
||||
Bundle/policy only is a valid smaller proof artifact, but requires a later parse/admit step to recover the graph and warnings for adoption. It is preferable only if the deliberate next consumer needs exclusively bytes and no diagnostics/model. That does not appear to match the currently described editing goal. Moving existing values after verification is smaller than introducing another cache, clone or deferred parse protocol.
|
||||
|
||||
The accepted model must be the resolver's actual output model, not a clone/move of desired intent merely because equality passed. Whole equality is the gate, not a replacement constructor. Existing model/serde/raw-validator heap exclusions permit retaining that already admitted Project without adding its internal maps/strings to logical auxiliary W; call this an explicit inherited domain, not a claim that the Project occupies no memory.
|
||||
|
||||
## Exact retained-warning inventory
|
||||
|
||||
`mixed/project.rs:594–649` reserves each mapped diagnostic before its path copy/destination push. The reservation is one entry, zero occurrences, and byte lengths of path + code + optional field + message. Severity/object UUID/span/line/column are scalar; no copied text is hidden there. `source_project.rs:80–84` defines `SourceDiagnostic { path, diagnostic, related }`; mapper always creates empty related. The return at project.rs:651 rejects any Error-severity issue before returning a resolved success.
|
||||
|
||||
For the current successful producer, compute without allocations:
|
||||
|
||||
```
|
||||
W.entries = warnings.len()
|
||||
W.occurrences = 0
|
||||
W.text = checked sum over warnings of
|
||||
path.len + diagnostic.code.len + optional field.len + message.len
|
||||
```
|
||||
|
||||
Use the same Slots checked arithmetic as the mapper, not saturating sums or a numeric total copied from a previous phase. Require every related vector empty and every severity non-Error under this sealed extraction route. An unexpected shape is an invariant refusal before returning the accepted owner, never silently ignored. If later mapper changes introduce related locations, update the named inventory (entries and path bytes) and tests together; this choice does not authorize flattening/dropping them.
|
||||
|
||||
A checked borrowed loop over current warnings allocates no extra frontier or String. Do not construct a secondary warnings Vec, clone diagnostics, format the Project, or rerun validation to count them. Current storage ledger already contains these warning reservations while syntax and equality are live; **do not reserve W a second time there**. Reconstruction for final transfer is distinct from additive growth.
|
||||
|
||||
## Safe transition and accounting
|
||||
|
||||
Recommended logical sequence:
|
||||
|
||||
1. Resolve the actual complete output with carried O under exact inherited policy; keep resolver and warnings live.
|
||||
2. Reserve the selected actual-model equality scratch against the full live ledger, compare the entire actual Project with desired, and release E. Retaining warnings means they must already participate in that equality-time aggregate, as they currently do in resolver storage.
|
||||
3. Compute W from the current warning records and checked final `O + W`. Validate that final total under the resolver's original Budget while it still exists. It should be a subset of the successfully held full storage; a componentwise subset check is a useful internal invariant, not a substitute for correct ownership inventory. If checking fails, return diagnostics with the existing failure escape and drop output, never a usable partial owner.
|
||||
4. Destructure/move actual Project and warnings into local owners, explicitly drop all remaining syntax/index/expression/occurrence/catalog/guard state, then move pending SourceBundle/policy into the accepted owner. Rust must establish that the guard borrowing pending is gone before its bundle moves. No copied warning path/model is necessary and no self-reference remains.
|
||||
|
||||
Final retained entries are O.entries + warning count; text is O.text + W.text; occurrences are O.occurrences (normally zero for the emitted bundle) plus zero. Do not carry released syntax U/M/T, equality E or resolver catalogs as live accepted storage. Conversely, do not return O alone when warnings remain owned. Model and original desired borrow do not become new logical slots merely by this ownership transition.
|
||||
|
||||
An independently computed final subset check can happen before physically dropping other objects because it is checking the eventual destination inventory, not claiming those drops already happened. The full resolver ledger must continue to cover coexistence until actual drops. An implementation that resets that live ledger to O+W early and subsequently allocates against its artificial headroom would be incorrect.
|
||||
|
||||
All accepted-result fields should remain private with read-only accessors. No public unchecked bundle+Project+warnings+Slots constructor, mutable bundle/model access, or from-desired shortcut. Any later consuming adoption method needs its own invariant about jointly installing graph/source/warnings. Read-only accepted ownership itself does not authorize saving/history mutation.
|
||||
|
||||
## Finite evaluators and caveats
|
||||
|
||||
Use a real emitted output that resolves with at least one stable mapped warning. Independently derive warning entry/text bytes from its literal expected path/code/field/message; assert warning content/location unchanged, actual Project equality and exact final O+W. A warning-free fixture alone cannot detect the dropped-W bug. Include multiple warnings or an optional field only if needed to distinguish summation from one-record handling; do not expand into a full validator redesign.
|
||||
|
||||
Keep the equality-time warning reservation and final retention tests separate from output verification caps: final O+W is usually below the already admitted resolver/equality peak, so a purported final-only cap-minus-one failure may be impossible through a real earlier-admitted owner. Use a checked-inventory fault control for impossible overflow/related-shape defects and label it accordingly; retain a real profile unchanged through actual pipeline positives/refusals. Do not lower the policy after admission to fabricate provenance.
|
||||
|
||||
Warnings should stay actionable after syntax drops: source path, span, Unicode line/column and optional field must still refer to the verified returned bytes. Moving them preserves the mapper's positions; rewriting output after extraction would invalidate that relation. An independently matching desired Project alone cannot establish diagnostic-to-byte identity.
|
||||
|
||||
Optionally retain already available scalar new-snapshot Usage/static admission metadata if a concrete caller needs it, but it is not necessary to retain expression arenas or whole syntax just to avoid losing those scalars. Original work and verification work must remain separately identified. No new arbitrary constructor is justified by exposing counters.
|
||||
|
||||
I see no correctness blocker in moving actual Project plus warnings with explicit O+W transfer. The material obligations are exact warning inventory, no duplicate charge, full-ledger coexistence through equality/drop, and sealed immutable construction. This recommendation leaves actual equality-frontier proof and output-admission behavior to their separate accepted contract/review.
|
||||
@@ -0,0 +1,48 @@
|
||||
# Remaining parser scratch: bounded independent critique
|
||||
|
||||
Read-only source basis97a8703adc75467a7f8e75f5c6cb00ca542b707c. Entry-producer work is pending and is not assumed accepted or executed here. This is a recommendation for the parent to decide, not an accepted domain change or runtime finding.
|
||||
|
||||
## Conclusion and obligation
|
||||
|
||||
Yes: an output-acceptance contract can explicitly preserve existing independent file/token/grammar/numeric bounds for named remaining parser scratch instead of adding all that scratch to the carried aggregate. This is consistent with the bounded logical domain, provided the contract accurately names its timing and exclusions. It must not claim one global physical memory ceiling, aggregate before-every-allocation, or that an independent bound is a reservation against the surviving output owner.
|
||||
|
||||
The user need is reliable graphical/text/API editing and persistence. A physical heap meter is not a user-owned requirement identified in the domain audit. The accepted rename-plan contract's mandatory successor *does* require complete actual emitted output, exact inherited policy, surviving-output accounting, actual parse/resolve/admission, whole desired-Project equality with E scratch, documented old-owner transition, and no usable failed result. These are not waived by choosing a smaller scratch domain. That contract does not name every parser coordination stack as a new carried-ledger allocation.
|
||||
|
||||
The earlier `CARRIED-STORAGE-DOMAIN-AUDIT.md` calls itself a design audit, not an accepted contract. Its recommended expansion seams and question about traversal/delimiter scratch must not silently become user requirements. In particular, its suggested significant-token push charge is not the currently accepted metric: the entry contract explicitly retains token storage under the token bound, with one token-end M subtotal reserved before ends construction. The newer text contract explicitly names its retained String producers and temporary decode/result strings. Neither permits hiding a newly introduced in-scope owned output/path/equality structure as old scratch.
|
||||
|
||||
## What existing source actually bounds
|
||||
|
||||
All source references below are repository-relative at97a8703.
|
||||
|
||||
| Scratch | Existing guard or structural bound | Remaining limitation |
|
||||
| --- | --- | --- |
|
||||
| Folded path table and traversal paths | `source_project/traversal.rs:22–87` checks file count and source-text sum before its folded table; validates every supplied path. Active/visited unique paths are related to file count; pending includes to include-edge count. | File-count bounds are not total path-byte bounds. Folded Unicode path copies and canonicalization allocate outside aggregate T. `include` at149–178 resolves/copies target and location before its edge-limit refusal; it is false to describe every such temporary as guarded before growth. |
|
||||
| Traversal queue/children/active stack | Same file/edge/depth policy; `next` checks depth before visited insertion; `finish_file` transfers pending children into stack (`89–186`). | Repeated include targets still consume edges and path copies. Entry count and path text can coexist with output C without debiting its aggregate ceiling. A deep/wide graph is not bounded solely by visited-file count. |
|
||||
| Delimiter stack | `language.rs:1032–1040`: at most one push per opening delimiter in the already scanned significant-token sequence. Ends array is the selected M subtotal. | Delimiter stack is another collection, not prepaid by the same N ends reservation. Token guard is a separate finite bound; no aggregate C+M charge for delimiter slots exists. |
|
||||
| Ordinary open declarations | `language.rs:1539–1723`: stack carries indices into the flat declaration vector. Existing declaration events/model count checks precede each declaration and its open push. | Maximum live depth is structurally at most declarations. Do not claim this vector is controlled by typed-value depth64 or by the typed Pratt frame budget; those are different controls. |
|
||||
| Pratt coordination | `analysis_source/syntax.rs:18–45,60–63,154–264`: tasks/values/sequence coordination are iterative; expression/operator/argument events, raw tokens and relevant grammar-frame checks constrain growth. | Not every Vec push has its own reserve, and grammar frame count is not native stack bytes. Completed subtree values remain owned while siblings parse. Existing small-stack successes are finite evidence, not a universal stack guarantee. |
|
||||
| Typed duplicate-name/ID sets | `declarations.rs:284–307,604–624` and `syntax.rs:238–249`: entries track already charged declarations/arguments/overrides. Named string clones derive from parsed reference/name text. | These clones are explicitly excluded scratch, not retained T or scanner/grammar decode W. Work count bounds their number, not independently their total copied bytes; original text/source bounds provide an additional constraint. No existing aggregate reservation covers their overlap. |
|
||||
|
||||
These are finite structural relationships, not exact peak formulas. With unlimited/wide configured policies, they may be practically large. UTF-8 case folding, path normalization, repeated path copies, large sequence coordination and native enum sizes are reasons not to present the logical ceiling as a process-memory bound.
|
||||
|
||||
## Choices
|
||||
|
||||
1. **Retain named independent bounds, explicitly excluded from carried W.** Smallest next step. Complete selected retained T/U/M hooks and reconciliation; retain exact existing traversal/token/grammar checks and refusal semantics; finish owning actual-output admission/equality. Document the table above in the acceptance contract. This is my recommendation for the currently stated editing objective.
|
||||
|
||||
2. **Add selected scratch W, starting with a demonstrated problem.** Useful if a specific unbounded or disproportionate copy threatens the intended deployment, or if the parent selects a stronger aggregate resource promise. For example, traversal path storage would require canonicalization/folded-path temporary and queue ownership decisions, with path-byte rather than just file-slot accounting. A delimiter-only W is simpler but proves nothing about the other excluded structures. Each addition needs before-growth placement, release/transfer rules and a literal peak oracle; it is not a harmless extension to current caps.
|
||||
|
||||
3. **Universal parser/all-heap containment.** Much larger and unsupported as the automatic next prerequisite. Allocator capacity, model/serde/validator and numeric internals are already excluded by accepted contracts. Instrumenting only parser stacks would still not establish this promise.
|
||||
|
||||
Do not choose option1 while retaining prose from option2 that says all parser auxiliary storage is charged against C. Conversely, do not block actual-output correctness on proving an unrequested universal bound.
|
||||
|
||||
## Concrete evidence needs without scope expansion
|
||||
|
||||
Before relying on the existing bounds, source review must establish each structural relationship and no new output-verification helper bypasses its existing checks. Reuse existing limit controls where they already cover duplicate include edges, depth, token exhaustion, declaration nesting and wide/deep expressions. A missing relevant control is a reason for one finite diagnostic test, not a generic scratch allocator framework.
|
||||
|
||||
The clearest risk deserving a specific design note is traversal path text: source-byte sum excludes map keys, and include canonicalization occurs before edge refusal. The accepted owned-output C already counts all emitted path keys and entry text, but that does not account folded/path queue copies. Either explicitly retain this limitation, or select a separate path-specific strengthening. Do not claim file/source limits alone provide a tight path-byte peak. No measured failure in this review requires instrumentation immediately.
|
||||
|
||||
New accepted-output lifetime/equality scratch is different: it is explicitly mandated and cannot be excluded as parser coordination. Preserve old B during emission, carry actual O after the documented old-owner drop, include new verification syntax/storage against O, reserve E against the correct verification live total, and drop temporary verification before returning a non-self-referential owning accepted result. An excluded raw desired/model borrow does not justify losing in-scope path/output copies. Retained mapped diagnostics continue to require their existing reservations even though transient refusal diagnostics have an escape.
|
||||
|
||||
Native stack risk remains empirical. Preserve existing128KiB/deep/wide controls and inspect changed prologues if new state/hooks alter frames. A stack failure is not repaired by putting vector slots in W; neither a phase check nor a token bound proves Rust stack headroom. Do not relax tests to support a stronger claim.
|
||||
|
||||
Suggested precise acceptance wording: “The verifier checks the declared retained syntax and auxiliary domain, plus the named temporary decode and equality domains, against the exact inherited aggregate policy including surviving output. Existing traversal, delimiter, ordinary-open, Pratt and duplicate-name scratch retain their independent file/edge/token/grammar bounds and are excluded from that aggregate metric. This is not a physical-memory or before-every-allocation guarantee.” Actual source/evidence must support the selected hooks before that wording is claimed implemented.
|
||||
@@ -0,0 +1,97 @@
|
||||
# Pending output ownership source map — proposal only
|
||||
|
||||
Source inspected: `a629ae01d49bb3cb482a047eb41832ad4246a5c6`; no compilation,
|
||||
source modification or acceptance claim. This refines only the lifetime seam in
|
||||
[ACCEPTANCE-PROPOSAL.md](ACCEPTANCE-PROPOSAL.md), not equality/scratch policy.
|
||||
Paths below are relative to `.local/as3batch` at that commit.
|
||||
|
||||
## Existing authority
|
||||
|
||||
`crates/oa-core/src/language/analysis_source/mixed/project/ordinary_index/rename/emission.rs:6`
|
||||
owns PendingRename's bundle, exact Limits/ProjectLimits, retained Slots, original
|
||||
expression count and immutable desired Project reference. Its constructor at the
|
||||
end of `emit_changed_unverified` follows output reservation and explicit old-owner
|
||||
drop; no public bundle/slots constructor exists. Those private fields currently
|
||||
prevent sibling carried code from reading authority.
|
||||
|
||||
`.../mixed/carried.rs:40` consumes CarriedInput into a bundle reference, one policy
|
||||
value and a slim guard. `:63` derives C from that guard; `:116` selects both producer
|
||||
modes. `.../mixed.rs:39` stores the guard alongside the borrowed bundle and Budget;
|
||||
`:308` is the shared parser. Consumed resolution at `.../mixed/project.rs:453`
|
||||
retains that syntax. None of these owners currently needs desired's lifetime.
|
||||
|
||||
## Recommend Candidate A with a complete pending reference
|
||||
|
||||
The smallest variant avoids projecting authority down to a bare `&Slots` at all:
|
||||
|
||||
```rust
|
||||
// Private variants in the existing enums; no new allocating wrapper.
|
||||
SnapshotBacking::Pending(&'s PendingRename<'s>)
|
||||
GuardBacking::Pending(&'s PendingRename<'s>)
|
||||
|
||||
impl<'s> CarriedInput<'s> {
|
||||
pub(super) fn from_pending<'t: 's>(p: &'s PendingRename<'t>) -> Self {
|
||||
Self { backing: SnapshotBacking::Pending(p) }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This is a proposed Rust shape, not a compiler-tested patch. PendingRename is
|
||||
covariant in its sole lifetime: the only use is `desired: &'t Project`, and all
|
||||
other fields own data without that lifetime. Consequently the longer desired
|
||||
borrow can be shortened to the pending borrow `'s` when storing `p`. The original
|
||||
pending value retains its original type/lifetime; no data moves or self-reference
|
||||
is created. Parser, resolved and indexed owner types keep their single existing
|
||||
`'s`. Immutable `&PendingRename` itself prevents mutation, consumption or dropping
|
||||
its bundle while syntax/guard remain usable. No trait object or PhantomData proof
|
||||
is needed, and future invariant lifetime-bearing fields would require revisiting
|
||||
this coercion rather than silently bypassing it.
|
||||
|
||||
Add narrowly visible immutable Pending methods `bundle() -> &SourceBundle`,
|
||||
`policy() -> (Limits, ProjectLimits)` and `retained() -> Slots`, restricted to
|
||||
`pub(in crate::language::analysis_source::mixed)`. CarriedInput accepts only the
|
||||
complete pending reference, never those components as independent arguments.
|
||||
`into_parts` derives bundle/policy from its stored pending and transfers the same
|
||||
reference into GuardBacking::Pending; `slots()` calls that pending's retained
|
||||
method. Policy is copied once into the existing preparse transfer value and then
|
||||
Budget/syntax limits; the guard contains only one pointer, not another policy.
|
||||
Pending necessarily retains its original immutable policy as owner provenance.
|
||||
No recomputation of C, tuple authentication or runtime identity comparison helps.
|
||||
|
||||
## Candidate B and visibility
|
||||
|
||||
Embedding an owned RetainedSnapshot in Pending would remove the cross-reference
|
||||
between carried and emission modules and also avoid desired lifetime covariance.
|
||||
It adds no necessary heap allocation, but replaces existing pending fields,
|
||||
changes emission construction and existing direct inspection tests, and requires
|
||||
a controlled production snapshot constructor or wider sibling field authority.
|
||||
That larger migration has no present ownership benefit over the complete pointer.
|
||||
Prefer B only if later genuinely shared owning snapshot behavior justifies it.
|
||||
|
||||
The module paths are already visible: `ordinary_index.rs:433` exposes rename to
|
||||
analysis_source, and `rename.rs:2` exposes emission likewise. Pending type itself
|
||||
has that visibility. Keep carried private to mixed; descendants can name its
|
||||
module. The needed changes are just its from_pending entry point and a named
|
||||
strong parse entry with visibility up to mixed, plus the three immutable pending
|
||||
views. Keep SnapshotBacking/GuardBacking variants and fields private to carried;
|
||||
do not export SnapshotPolicy or add arbitrary production RetainedSnapshot inputs.
|
||||
|
||||
## Consuming verification and finite type checks
|
||||
|
||||
Implement verify in emission, where it can consume self and eventually move its
|
||||
private bundle. A local block calls the strong parser with from_pending(&self),
|
||||
consumes syntax into resolve, performs separately specified bounded equality,
|
||||
then drops resolved owner/guard before leaving that block. Only afterward move
|
||||
self.bundle into an owning accepted result. No parsed witness or desired borrow
|
||||
escapes, so accepted output has no lifetime parameter or self-reference. Failure
|
||||
returns diagnostics and drops self; successful no-op remains the separate old
|
||||
owner branch. This note does not select accepted-result extraction/installation.
|
||||
|
||||
Compiler acceptance must demonstrate: long-lived desired + local pending can
|
||||
produce a short scoped syntax; resolved owner cannot outlive pending; moving or
|
||||
mutating pending while the guard is subsequently used is rejected; dropping all
|
||||
verification borrows permits moving the bundle into the accepted owner. Positive
|
||||
runtime tests should drop original input and desired after acceptance and still
|
||||
inspect returned output. Preserve exact inherited policies and C once throughout;
|
||||
no new carrying-mode bypass or policy mutation. Any compile-fail examples should
|
||||
be narrow external snippets/appropriate harness, not runtime unsafe tricks.
|
||||
@@ -0,0 +1,60 @@
|
||||
# Independent unnamed-project warning oracle
|
||||
|
||||
Authored from existing mapper/catalog/index/emission logic at2941713 (relevant behavior unchanged from97a8703), not parser or runtime output. This is a prospective acceptance evaluator, not a passing execution claim. Literal-only Python checked lengths, hashes and offsets; no compiler/parser/binary ran.
|
||||
|
||||
## Exact original and output
|
||||
|
||||
Single file/entry `main.arcadia` (12 UTF-8 bytes each). Original71-byte source, including final LF:
|
||||
|
||||
```text
|
||||
arcadia 8;
|
||||
project p "" id "00000000-0000-0000-0000-000000000001" {
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
Original SHA256 `68b9215285d622c77b46df105839f65c40fb76f2ba7c55a6a9cbbc1fc4576146`. Rename only project aliasp→next; physical original alias span19..20. Output74-byte literal:
|
||||
|
||||
```text
|
||||
arcadia 8;
|
||||
project next "" id "00000000-0000-0000-0000-000000000001" {
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
Output SHA256 `dbde19f06d6bde26c16b4672dcc43036dec693fb04ebc23de76e4faa369dd81f`. O=(entries1, occurrences0, text98), since74+12+12=98. Exact inherited entries18 and bytes161 are the proposed unchanged positive policy; bytes160 is the proposed late-refusal sibling, with all other limits roomy and unchanged throughout each pipeline. Original parsing must be the supported legacy uncarried route because changed emission rejects carried syntax.
|
||||
|
||||
## Complete expected Project and warning
|
||||
|
||||
Actual accepted Project must have schema_version8, UUID00000000-0000-0000-0000-000000000001, alias`next`, name`""`, description`""`, and all six maps empty: definitions, elements, relations, diagrams, artifact_links, traces. Original Project differs only in alias`p`. No expressions or lowered occurrences exist, so E_actual=0 for equality.
|
||||
|
||||
Exactly one mapped diagnostic is expected:
|
||||
|
||||
- path`main.arcadia`; related empty;
|
||||
- severityWarning; code`unnamed`; object_idSome(ID1); fieldSome(`name`);
|
||||
- message`Give the project a descriptive name.` (36 bytes);
|
||||
- output span11..69, line2, column1.
|
||||
|
||||
`validation.rs:325–333` emits that warning for empty trimmed project name. `project.rs:666+` falls back to the project header for the name field; empty name does not manufacture a field span. Header start11 is unchanged, opening brace69 ends the output header. Original warning instead spans11..66. The returned warning must be the output warning, not one moved from original admission.
|
||||
|
||||
Current mapper `project.rs:594–649` makes W=(entries1, occurrences0, text59): path12 + code7 + field4 + message36. Scalar span/UUID/severity/line/column add no string bytes. Final accepted O+W=(2,0,157). Model payload remains the established excluded domain; this is not its physical memory size.
|
||||
|
||||
## Verification peak, with catalog lifetime
|
||||
|
||||
Output U1/M11/T4: one project syntax event, F1+ten significant tokens, aliasnext4 and empty name/description. Work bytes74, raw tokens19, references0, numeric units0. Combined retained syntax is entries13/text102=O98+T4. Scanner temporary36 fits134; grammar project UUID temporary36 overlaps alias4, requiring138. No ordinary positions, value maps or typed syntax exist.
|
||||
|
||||
Catalog creation reserves alias/owner2 entries with alias4 text, then table2 entries with another alias4 (`project.rs:240–325`). Its peak is entries17/text110. `project.rs:521–524` drops aliases before lowering; `557–560` drops tables before warning mapping. Thus neither extra alias copy remains at the warning reservation. The owner table remains one entry, no text, through mapping, and drops afterward (`655`).
|
||||
|
||||
Immediately before warning mapping: entries14=syntax13+owner1, text102. The mapped warning reserve adds1/59, reaching entries15/text161. At bytes160 the exact refusal is `Source8 index/output byte limit exceeded.`, codeinvalid_source, pathmain.arcadia, span11..69, line2column1, no related diagnostics. This typed mapper failure preserves the full header span; it is not the ordinary parser's one-byte transport. Bytes161 admits this reservation. Owner table then drops, leaving resolver entries14/text161 =O + U/M/T + W. EqualityE0 adds nothing. After syntax drops, retained accepted inventory is entries2/text157.
|
||||
|
||||
The final O+W check is not itself the161-byte peak and must not be called the site of160 refusal. It rechecks the smaller eventual retained subset. The intended negative occurs before warning destination allocation while original output-owned C is still live.
|
||||
|
||||
## Original predecessor clearance under the same policy
|
||||
|
||||
Original uncarried syntax has U1/M11/T1. Its name is empty; original mapped warning has the same59-byte string inventory. Catalog peaks at syntax12+4=16 entries and text3; at warning mapping it retains syntax12+owner1+warning1=14 entries/text60. Owner release leaves13 entries/text60. Typed index adds one retained global-name entry/text1, giving14/text61. Ordinary authored index adds one header occurrence entry; its one-global reverse map is temporary, so final authored state15/text61. No expression/occurrence/field work adds another slot.
|
||||
|
||||
Plan stores one changed-name entry and one physical edit entry; replacements borrow desired and add no owned text. Its final state is17 entries/text61. Collision-check one-element namespace occurs before the edit exists, so it does not exceed17. E0 adds no equality scratch. Old emitted-output overlap is17+O1=18 entries and61+98=159 text. Both bytes160 and161 therefore clear old parser/resolver/index/plan/emission; source/output byte lengths71/74 also fit. Emission drops old owner/warning before verification, so the old59 warning bytes do not coexist with the new warning map.
|
||||
|
||||
This derivation agrees with the existing named empty emission envelope after removing seven old name bytes and adding one59-byte/one-entry original warning. It is nevertheless an independently traceable source calculation, not a runtime result. Do not introduce a default policy, a newly tightened pending owner, or a borrowed numeric baseline to make the negative pass earlier stages.
|
||||
|
||||
Expected real end-to-end distinction: identical entries18 and other policies, bytes161 succeeds with exact output/Project/warning/O+W; bytes160 reaches actual-output warning mapping and refuses there. If runtime rejects earlier, investigate actual source/lifetime/count differences rather than changing the oracle from observed output. Additional copy/reservation introduced by the future acceptance implementation must be disclosed and independently justified before claiming this cap.
|
||||
@@ -0,0 +1,121 @@
|
||||
# Mixed source8 command-line checking
|
||||
|
||||
Selected next outcome in the already-owned textual-language scope: an engineer
|
||||
can run `arcadia check-mixed ROOT [--json]` on an explicit entry file and receive
|
||||
located results for its complete included mixed model, without changing files.
|
||||
Baseline source is public-read checkpoint bab6f96a. This contract is local-only
|
||||
while Gitea writes/publication remain unavailable. The published preview is frozen.
|
||||
|
||||
## User-visible behavior
|
||||
|
||||
Keep existing check/lint/stabilize/rename behavior and the ordinary library
|
||||
`prepare` contract. Add one explicit check-mixed spelling, rejecting --write,
|
||||
unknown/repeated options and invalid argument counts. Route it separately from
|
||||
editable preparation; do not construct a fake Prepared or SourceIndex. Use a
|
||||
closed command-dispatch enum rather than an ambiguous combination of mode flags.
|
||||
|
||||
The existing Report envelope remains version1. Operation is check-mixed; success
|
||||
is checked/exit0, with advisory warnings permitted. Changes is empty, written is
|
||||
false, migration is null, identities_to_stabilize is zero. All locations refer to
|
||||
captured original input. Missing explicit UUIDs, unsupported versions, malformed
|
||||
syntax and semantic invalidity are errors, not implicit stabilization or migration.
|
||||
Retain actual warning order and authored object identities from the admitted model.
|
||||
|
||||
Use the existing exit conventions: source/semantic rejection1, arguments2,
|
||||
disk/path/journal/output failure3. Match existing treatment of loader diagnostics
|
||||
and disk error variants, including its source-limit distinctions; document/test
|
||||
the actual relevant precedence instead of assuming every limit uses one phase.
|
||||
Human output/help must explain the explicit-ID schema8 read-only contract and
|
||||
make the check result clear without implying proposed edits.
|
||||
|
||||
## Source discovery and ownership
|
||||
|
||||
Add `source_files::load_mixed_source(entry, limits)` returning an opaque
|
||||
MixedSourceSnapshot with immutable bundle() and diagnostics() views. It captures
|
||||
original bytes only. It confers no save authority, editable compilation, recovery
|
||||
or promise of an atomic snapshot against another process. A caller must handle
|
||||
discovery diagnostics before treating the bundle as complete. CLI owns this
|
||||
snapshot, invokes check_mixed_bundle on its borrowed bundle only after discovery
|
||||
succeeds, and copies diagnostics into the owned report before dropping the result.
|
||||
|
||||
Reuse the existing private disk discovery loop and path/read/physical-alias/journal
|
||||
checks through a closed ordinary/mixed selector. The ordinary route remains
|
||||
unchanged, including overlays, detached buffers, expectations and final compilation.
|
||||
The new fresh mixed route has no overlays and skips ordinary compile_bundle.
|
||||
Consume its private DiskProject storage into bundle/diagnostics and discard unused
|
||||
expectations/root/reachability after the checks; avoid cloning reachable_bundle or
|
||||
exporting DiskProject as mixed save authority.
|
||||
|
||||
Before disk reads, clamp the mixed source-byte allowance to min(caller,64MiB),
|
||||
matching the parser's hard ceiling; preserve all other caller restrictions.
|
||||
Keep canonical root-relative paths, UTF-8, regular-file/link rules, case and
|
||||
physical-alias detection, aggregate files/bytes/edges/depth and pending journals.
|
||||
Pending transactions remain an error and are never recovered implicitly.
|
||||
Resolve includes with the existing resolver. Parse each successfully loaded path
|
||||
once in existing deterministic traversal order; count repeated authored edges.
|
||||
Complete semantic checking still detects cycles and cross-file errors.
|
||||
|
||||
Inspect includes by the real shared mixed Parser::unit grammar, including typed
|
||||
declarations, explicit IDs and long-form refusals. Centralize the minimal parser
|
||||
setup/operation needed by discovery and complete parsing; preserve the latter's
|
||||
grammar, producer hooks, policy, retained witnesses and stack behavior. No regex
|
||||
scanner, second grammar or complete-checker call on partial bundles is permitted.
|
||||
Return no partial include list after a unit syntax failure.
|
||||
|
||||
## Bounded discovery phase
|
||||
|
||||
Choose one shared discovery Budget and ModelCounts for the entire discovery pass,
|
||||
derived by the existing mixed source profile. Enable existing text and entry
|
||||
producer checks once with zero initial reservation. Byte/token/reference/numeric
|
||||
work and syntax/model counts accumulate across files; do not restart allowances
|
||||
per file. Charge each inspected file before lexing/parsing and keep the existing
|
||||
prospective syntax/value/owner producer checks. Tokens keep their existing
|
||||
pre-scan bound; disk/path/traversal tables retain their separate filesystem limits.
|
||||
Do not allocate edit positions or a resolved/indexed model merely for discovery.
|
||||
|
||||
Completed unit syntax is dropped after moving its include information. Its
|
||||
remaining producer charges deliberately remain reserved until this phase ends:
|
||||
these counters are a conservative cumulative admission bound, not a claim of
|
||||
exact live storage or a verifier-owned carry. Ordinary in-parser temporary and
|
||||
UUID/key releases remain unchanged. No unit-drop refund mechanism or new generic
|
||||
allocation framework is needed. Shared grammar and once-per-path order consume
|
||||
the same syntax/work events as full parsing, with fewer later projections; tests
|
||||
must discriminate cumulative accounting from accidental per-file resets.
|
||||
|
||||
A syntax/budget failure is terminal for mixed parsing: record its original located
|
||||
diagnostic, do not reuse a poisoned span-only Budget for another file, and do not
|
||||
invoke final model checking on incomplete discovery. Still perform required final
|
||||
filesystem and journal checks. Ordinary aggregation remains unchanged.
|
||||
|
||||
After successful discovery, drop the discovery budget/scratch and run the existing
|
||||
complete checker with a fresh profile and the original caller policy. These are
|
||||
two explicitly bounded passes, not a claim of one shared whole-command numeric
|
||||
allowance or all-heap accounting. Discovery grants no semantic admission witness.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Real executable and library cases load mixed OA/SA multi-file fixtures with
|
||||
cross-file/scoped typed references and nested ordinary records. Independently
|
||||
construct complete expected models for those same bytes; check actual complete
|
||||
JSON/human diagnostics and status rather than inventing a model field in Report.
|
||||
- Prove exact source/directory preservation on success, warning and refusal,
|
||||
including Unicode/CRLF and comment/string text resembling includes or aliases.
|
||||
Exercise missing UUID classes and wrong-kind/unresolved typed references.
|
||||
- Test missing/cyclic includes, root/path escape, case and physical aliases,
|
||||
invalid UTF-8/nonregular files, pending journal, and selected exact file/byte/
|
||||
depth/edge boundaries. Platform-specific checks are claimed only when run.
|
||||
- Observe the actual shared discovery constructor/counters: multi-file cumulative
|
||||
work, retained conservative reservations after dropping a unit, zero temporary
|
||||
balance after successful units, exact caller/profile capture and first-error
|
||||
location with an unparsed later sibling. Use real small boundary controls with
|
||||
positive cases; test-only tightening must not become public policy.
|
||||
- All old core and relevant filesystem/source/stack tests remain passing. Run
|
||||
existing ordinary CLI tests plus new mixed CLI tests, actual CLI commands,
|
||||
strict core/CLI Clippy, doctests and formatting disposition. Bind gates to a
|
||||
reviewed clean source commit and preserve initial failures without weakening
|
||||
model, bytes, locations or resource expectations.
|
||||
|
||||
Independent source/runtime review follows implementation. Parent owns integration,
|
||||
compiler execution and acceptance. Core discovery/parser and CLI/report/tests/docs
|
||||
may be delegated with disjoint files and the interfaces above. Linux remains
|
||||
user-deferred; no GUI/editor/save/history or release integration is implied.
|
||||
@@ -0,0 +1,48 @@
|
||||
# Read-only mixed CLI: independent critique
|
||||
|
||||
Research only against `bab6f96a5b05b37f2b5d67a430aed0f67d783e73`; no builds, application execution, production edits or remote actions. Reviewed implementation map SHA256 `cb611919a1d6b6463f5e7054922d40457734305656e0dfff9dd4a1388a2c4ed7`. References below are relative to that source. This selects a bounded next outcome, not acceptance of an implementation.
|
||||
|
||||
## Recommendation
|
||||
|
||||
Implement explicit `arcadia check-mixed ROOT [--json]`: discover actual included files safely, then invoke the existing complete borrowed checker on captured original bytes. Require schema8 and explicit UUIDs, including ordinary declarations in this command. Return diagnostics through the existing report envelope, with no changes, migration, stabilization or write. This delivers useful command-line validation for the textual-model/linting goal in `PROJECT_GOAL.md:64–69,96`, while leaving editable model adoption separate.
|
||||
|
||||
`check --mixed` is also coherent, but offers no semantic advantage and requires more option-combination rules. Choose one spelling. Automatic replacement of `check` is unsafe: existing ordinary drafts intentionally accept omitted IDs and older versions. Schema8 alone cannot select between those policies. A manifest of every file is a simpler parser-free discovery interface, but transfers routine include enumeration to users and is not an equivalent root-file workflow. Full compile/index adoption is substantially larger and unnecessary here.
|
||||
|
||||
## Simpler correct discovery policy
|
||||
|
||||
Prefer **one shared discovery Budget and ModelCounts, with conservative completed-unit reservations retained until discovery ends**. This is simpler than the map's proposed exact release/reconciliation seam. Do not describe its counters as exact live storage, an admitted syntax witness, or carry into the final verifier. Ordinary and public in-memory checker accounting must remain unchanged.
|
||||
|
||||
The source supports this narrower policy:
|
||||
|
||||
- `analysis_source/mixed.rs:390–422` performs lexing and the shared `Parser::unit` using one Budget/ModelCounts. `lex.rs:15–68` charges bytes, scanner events including trivia, and scanner temporary string work. State shares model counts and typed numeric parsing rather than creating a fresh per-file allowance.
|
||||
- Full parsing retains each completed unit, tokens and lexical owners, and additionally constructs positions (`mixed.rs:425–511`). Dropping the discovery unit does not itself decrement Budget. Leaving its final charges reserved therefore remains an upper bound on live producer-domain storage; it does not create dangling ownership or an authority witness. Keep normal existing temporary/UUID/provisional-key releases within the parser unchanged.
|
||||
- For the same successfully parsed files and depth-first order, discovery uses the same unit grammar, U/token/reference/numeric events and transient string decoding as full parsing. Omitting position construction removes nonnegative metadata/text charges. Initial or incremental discovery file-slot reservations must be no greater than the full parser's F file slots, and must not charge already-visited paths again. Under these conditions, retaining completed-unit charges causes **no additional producer-cap refusal relative to the corresponding successful full syntax parse**, which retains those units anyway. This conditional argument does not promise identical failure precedence for invalid input or disk discovery.
|
||||
- Both existing walkers push includes in reverse for source-order DFS (`source_files.rs:511–517`, `source_project/traversal.rs:179–183`). Preserve that order and once-per-path inspection. Repeated authored edges still count; repeated targets do not get another syntax budget.
|
||||
|
||||
Name the phases explicitly: one bounded discovery syntax pass, followed by one independently bounded complete verification pass. Derive both profiles from the same original caller policy; do not expose arbitrary production profiles. In particular, numeric work is capped per named pass, not falsely advertised as one 50,000,000-unit command-wide allowance. Discovery Budget, parser state and all ordinary/typed/token owners must be dropped before full verification starts. Only loader-owned sources, bounded include/path bookkeeping and diagnostics survive. Do not retain an unnecessary parsed model or clone a complete syntax witness.
|
||||
|
||||
This reasoning covers the existing logical producer domains, not all Rust allocations, path bookkeeping, model heap, allocator capacity or RSS. Resolved include targets/locations belong to the existing bounded loader domain; do not call their transfer a producer-ledger reconciliation. Discovery never invokes the resolver or its exact syntax-inventory check. No new refund mechanism is needed merely to make already-freed units disappear from a conservative reservation.
|
||||
|
||||
An alternative fresh unit budget with explicit remaining aggregate work caps could also be correct, but requires transferring numeric/event usage and separating remaining work from per-unit storage limits. That is more machinery than retaining reservations. Fresh unrestricted per-file budgets are rejected: they silently multiply numeric and other work ceilings by file count.
|
||||
|
||||
**Concrete failure obligation:** existing discovery continues after `inspect_includes` failures (`source_files.rs:519–521`), whereas `Budget::active` (`budget.rs:228`) returns a latched Error containing a Span but no filename. Reusing that failed budget for a sibling would risk reporting the old span against the sibling. Select terminal mixed syntax/work failure: keep its first located diagnostic and perform no further unit parses using that budget. Still perform the selected final filesystem/journal checks. Preserve ordinary-path aggregation unchanged. Specify first-failure behavior rather than attempting to claim diagnostic identity with complete in-memory validation.
|
||||
|
||||
## Disk ownership and safety
|
||||
|
||||
Reuse the existing logical-path, root-containment, regular-file, bounded-read, case/physical-alias and journal checks (`source_files.rs:111–325,536–549`). Before any read, enforce aggregate effective source bytes `min(caller.model.max_source_bytes, 64 MiB)`; checking only the caller's possibly unlimited ceiling and rejecting later in the parser is insufficient. Pass the original ProjectLimits to final verification, without silently widening or replacing them.
|
||||
|
||||
For a fresh load, consume the discovered DiskProject into an opaque read-only `{ bundle, diagnostics }` after final checks, dropping expectations/root/reachability. Every stored source was discovered from the entry; no overlay/detached-input preservation is needed. Avoid `reachable_bundle()` (`source_files.rs:59–72`), which clones all source strings. The existing temporary raw expectation bytes and UTF-8 strings are two bounded copies, not one accounted allocation; document reuse honestly. A smaller fresh-only storage owner may be a later optimization, not a prerequisite for this milestone.
|
||||
|
||||
Keep reserved journal names, pending-save refusal and no implicit recovery. Existing path checks do not establish an atomic multi-file snapshot or defeat every concurrent replacement race (`source_files.rs:177`); promise validation of captured bytes and no writes by this command. Do not broaden platform claims beyond actual tests. Fail discovery before invoking the complete checker on missing/invalid buffers.
|
||||
|
||||
Route this command before ordinary CLI `prepare` (`oa-cli/src/lib.rs:213–310`), which calls ordinary lint/ID analysis/compilation even for Check. An Arguments command branch is sufficient; do not add a fake SourceIndex, editable Prepared value, save conversion or mixed write option.
|
||||
|
||||
## Falsifiable acceptance
|
||||
|
||||
1. Real executable OA/SA multi-file fixtures: typed/scoped cross-file references, ordinary nesting, escaped includes, comments containing `include`, Unicode and mixed CRLF. Compare complete diagnostics/report bytes and independently authored complete models for the same captured literals. Hash files and directory entries before/after; reject `--write` and unknown options.
|
||||
2. Discovery-budget tests: two files each individually below a tightened private numeric/event cap but jointly over it; repeated target inspected once; repeated edges counted; one shared ModelCounts limit across files; exact cap/cap-minus-one boundaries. Test-only profile tightening must not become caller-controlled public authority. Compare discovery producer reservations with the full parser at equivalent successful prefixes/fixtures, including quoted provisional releases and omitted projection charges.
|
||||
3. Failure attribution: first file triggers a latched budget refusal while a sibling remains queued; assert the original file/span once, no sibling reinterpretation and no final checker invocation. Include missing IDs in project, ordinary declaration, typed definition and parameter, plus unresolved/wrong-kind references reaching final verification.
|
||||
4. Existing filesystem invariants: bounded aggregate reads even with unlimited caller bytes; include depth/edges/files, invalid UTF-8, nonregular files, escapes/reserved names, case/physical aliases, cycles and pending journals. Preserve ordinary loader/CLI results. Pin mixed diagnostic precedence where discovery differs; platform-specific link/FIFO cases remain conditional.
|
||||
5. Verify actual normal-library/CLI boundaries and relevant finite stack controls after helper extraction. No universal headroom or full authoring claim follows from these checks.
|
||||
|
||||
No design blocker remains if the contract adopts the conservative reservation meaning, terminal failure rule, effective pre-read byte cap, and separate fresh final verifier above. The implementation must demonstrate those conditions; this report does not waive existing parser limits or substitute discovery success for complete semantic admission.
|
||||
@@ -0,0 +1,164 @@
|
||||
# Mixed CLI read-only check: implementation map
|
||||
|
||||
Design only against public-read branch bab6f96a (parent reports accepted gates).
|
||||
No source edits, compiler or remote actions. Paths/lines refer to this source.
|
||||
|
||||
## Recommended user outcome
|
||||
|
||||
Add explicit `arcadia check-mixed ROOT [--json]`, requiring the public checker's
|
||||
schema8 and explicit UUIDs. Preserve check/lint/stabilize/rename-alias behavior.
|
||||
Use the existing report envelope with operation check-mixed, status checked or
|
||||
invalid, no changes, written=false, migration=null and identities_to_stabilize=0.
|
||||
The operation validates captured original bytes; it does not stabilize, export,
|
||||
format, index or save. Help must state this distinction and reject --write.
|
||||
|
||||
The command choice and discovery architecture are separate decisions. Merely
|
||||
adding a command enum variant does not make the existing loader support typed
|
||||
syntax. A real parser-consistent discovery seam is the prerequisite below.
|
||||
|
||||
## Current blockers and reusable code
|
||||
|
||||
- oa-cli/src/lib.rs:213 prepare loads source_files::load_project, then ordinary
|
||||
lint/analyze/compile (234–310). Operation::Check still returns Prepared holding
|
||||
CompiledSourceProject and a disk snapshot; it is not a read-only mixed route.
|
||||
- main.rs:76 dispatches all accepted arguments through prepare and optional write.
|
||||
Add a separate report-returning mixed-check branch before that branch. Do not
|
||||
forge Prepared or an empty SourceIndex. An explicit Arguments command enum can
|
||||
separate read-check from existing editable Operation, or use a distinct
|
||||
check_mixed flag/variant handled before prepare; avoid illegal write combinations.
|
||||
- source_files.rs:329 load_project creates a DiskProject then discover_missing.
|
||||
Discovery's DFS reads bounded bytes and tracks originals/expectations (389–494),
|
||||
but495 calls ordinary inspect_includes and529 runs ordinary compile_bundle.
|
||||
- source_project.rs:207–268 inspect_includes validates paths and calls ordinary
|
||||
language::parse_unit, so it rejects valid short typed declarations. Filtering
|
||||
that diagnostic cannot reveal the missing include graph safely.
|
||||
- mixed.rs:390–422 already lexes and invokes the shared Parser::unit with actual
|
||||
mixed State and explicit IDs, producing ordinary.includes plus typed syntax.
|
||||
This exact unit grammar can inspect includes without resolving global references.
|
||||
Extract/reuse this narrow unit operation instead of a regex, guessed brace
|
||||
scanner, second quote decoder or calling the complete checker on partial bundles.
|
||||
- source_files.rs:104–216 root_entry/check_logical/disk_path/read_bounded already
|
||||
enforce root containment, canonical logical names, reserved journal namespace,
|
||||
link/regular-file restrictions and bounded reads.240 check_snapshot and293
|
||||
check_physical_aliases preserve aggregate and physical-alias checks. Reuse these
|
||||
functions in one core discovery engine; do not replicate their security logic in CLI.
|
||||
|
||||
## Minimal safe ownership shape
|
||||
|
||||
Introduce a named read-only discovery entry in source_files, with a small private
|
||||
closed selector OrdinaryEditable / MixedRead for shared discovery dispatch.
|
||||
Existing public discover_missing/load_project select OrdinaryEditable and retain
|
||||
all current completion/lint behavior. MixedRead selects mixed include inspection
|
||||
and skips ordinary compile/stabilization; it still performs all disk/path/journal
|
||||
checks. No generic caller-provided parser/loader callbacks or new public framework.
|
||||
|
||||
Prefer an opaque owning `MixedSourceSnapshot` exposing only bundle() and
|
||||
load_diagnostics(), or a narrowly scoped `load_mixed_source` result owning those
|
||||
values. Do not advertise it as a save snapshot or provide conversion into editable
|
||||
CompiledSourceProject. Internally reusing DiskProject's captured bytes is possible,
|
||||
but returning DiskProject publicly would unnecessarily expose save/discovery
|
||||
operations and expectations. Keep original raw-byte expectations private if reused.
|
||||
This design adds a small wrapper, not a clone of every buffer for an extra model.
|
||||
|
||||
CLI owns the snapshot, borrows its complete reachable SourceBundle while calling
|
||||
language::check_mixed_bundle, maps warnings/errors into the existing Report, then
|
||||
drops CheckedMixed before dropping source. Report owns mapped diagnostic strings;
|
||||
no borrowed checker escapes and no self-referential source/result struct is needed.
|
||||
Report has no Project field: complete model evidence comes from independent core
|
||||
and CLI-library tests of those same literals, not an invented JSON assertion.
|
||||
|
||||
## Discovery parser work and limits: resolve before implementing
|
||||
|
||||
Discovery is an extra syntax pass; final borrowed checker remains the sole complete
|
||||
semantic admission. Its existing budgets must be fresh for that full pass; do not
|
||||
subtract discovery work from final counts or represent a successful partial scan
|
||||
as an admitted model. Re-reading/reparsing captured strings is explicit bounded work,
|
||||
not a claim that the checker itself reparses or that disk is reread between phases.
|
||||
|
||||
The mixed unit helper needs a genuine Budget and ModelCounts. A naïve new budget
|
||||
per file silently multiplies allowed discovery numeric/token/reference work by the
|
||||
file count; a single shared budget with all retained-text totals left charged after
|
||||
units are dropped misrepresents live storage. Therefore contract must choose a
|
||||
small explicit **discovery-phase work policy** and lifetime:
|
||||
|
||||
- Recommended: one shared discovery work budget across all captured file parses,
|
||||
cumulative bytes/tokens/references/numeric/syntax-event work, with per-unit syntax
|
||||
owners dropped after copying only include locations. Reuse Parser::unit and its
|
||||
resource checks. Name and check temporary per-unit metadata/text ownership before
|
||||
growth if selecting producer mode; reconcile/release it on unit destruction.
|
||||
This is a real small producer-lifetime seam to specify, not existing free behavior.
|
||||
- Simpler alternative: per-unit bounded inspection with a documented aggregate
|
||||
upper bound of max_files times its cap. It is finite but can be far larger than
|
||||
the selected checker work profile; do not choose it silently or describe it as
|
||||
inheriting one whole-project numeric budget. Parent may reject that cost policy.
|
||||
- Do not retain all parsed witnesses then clone/reparse the complete bundle merely
|
||||
to avoid accounting a dropped unit. That grows live state and complicates source
|
||||
lifetimes for no consumer benefit. Do not stream-load into a borrowed BTreeMap
|
||||
while previously parsed units retain references into it; ordinary Rust borrowing
|
||||
disallows this without additional ownership infrastructure, which is unnecessary.
|
||||
|
||||
The implementation contract should pin the selected discovery bound/diagnostic
|
||||
order and helper's drop/refund rules before source edits. This is the main remaining
|
||||
bounded design question, not an argument for full editable-source adoption.
|
||||
|
||||
## Filesystem and diagnostic contract
|
||||
|
||||
Preserve existing root/file checks, UTF-8 rejection, aggregate byte/file/edge/depth
|
||||
limits, case/physical alias refusals and pending-journal detection. Root depth0;
|
||||
include paths resolved by existing resolve_include; count repeated authored edges.
|
||||
Final complete check still rejects cycles/semantic failures using its exact original
|
||||
coordinates. Discovery diagnostics prevent invoking checker with missing/invalid
|
||||
buffers. Return exit3 for disk/path/pending-journal failures under existing CLI
|
||||
mapping; exit1 for source/semantic rejection,0 for valid with advisory warnings,
|
||||
2 for invalid arguments. Pin mixed loader's diagnostic precedence in tests rather
|
||||
than assuming it equals in-memory checker's earlier/later limit branch.
|
||||
|
||||
Do not suppress pending_save simply because operation is read-only: existing CLI
|
||||
refuses it, and the captured files may represent interrupted save state. No journal
|
||||
recovery is implicit. Neither old nor new read path promises atomic multi-file
|
||||
snapshot/CAS against concurrent external replacement (source_files.rs:177 comment).
|
||||
Promise unchanged bytes by this command and validation of captured bytes, not
|
||||
that the disk still equals that snapshot after a concurrent editor writes.
|
||||
|
||||
## Compared/rejected options
|
||||
|
||||
- Automatically replace ordinary check with mixed: breaks existing omitted-ID and
|
||||
migration behavior. Schema8 alone does not distinguish ordinary drafts from the
|
||||
stricter explicit-ID mixed contract. Avoid heuristic detection/fallback on errors.
|
||||
- `check --mixed`: coherent alternative explicit spelling, but adds an option whose
|
||||
invalid combinations must be excluded for mutations. Dedicated check-mixed makes
|
||||
the limited outcome easier to explain; select one spelling, not both initially.
|
||||
- Explicit all-files CLI manifest/bundle: can reuse checker immediately, but forces
|
||||
users to enumerate includes and introduces another input/argument format. It is
|
||||
useful only if a real pipeline already supplies bundles; not the recommended
|
||||
ordinary filesystem workflow and not a substitute for safe include loading.
|
||||
- Full compile_bundle adoption: requires typed stabilization/index/update/formatter
|
||||
and application consumers; none is necessary for this read-only outcome.
|
||||
|
||||
## Finite acceptance and scoped files
|
||||
|
||||
Core changes: source_files shared discovery selector + read-only snapshot;
|
||||
language mixed unit inspection helper and narrow reexport for core-only use;
|
||||
source_project include-location conversion reuse where appropriate. CLI lib
|
||||
arguments/report path + main dispatch, docs/source-cli.md/help, focused tests.
|
||||
No changes to updater/save/formatter or public CheckedMixed authority.
|
||||
|
||||
Test actual executable on independently authored OA/SA three-file fixtures with
|
||||
cross-file typed/scoped references and ordinary nesting. Exact complete JSON
|
||||
warning/error reports, raw original byte hashes/directory listing unchanged after
|
||||
each command, including CRLF/Unicode and comments mentioning include/aliases.
|
||||
Core tests compare independently authored full models for those same bytes.
|
||||
|
||||
Exercise missing/cyclic include, invalid path/root escape, case/physical aliases,
|
||||
nonregular/invalid-UTF8 file, pending journal and exact aggregate/depth/edge limits.
|
||||
Only claim platform-specific link/FIFO tests where cfg permits; Linux remains
|
||||
user-deferred. Exercise missing IDs in all four classes and typed unresolved/wrong
|
||||
kind errors. Verify --write rejection and all existing ordinary CLI commands remain
|
||||
unchanged. No fake success for an empty include graph or wrong command dispatch.
|
||||
Add unit-hook counter/lifetime evidence for the chosen discovery work policy and
|
||||
repeat relevant existing filesystem/stack regressions after shared-loop factoring.
|
||||
|
||||
Parent decisions needed: command spelling; explicit discovery work/storage policy
|
||||
above; whether reuse of private DiskProject storage under opaque wrapper is worth
|
||||
its duplicated captured bytes versus a smaller read-only buffer owner. No source
|
||||
or compiler work is authorized by this proposal.
|
||||
@@ -0,0 +1,128 @@
|
||||
# Private mixed rename emission prerequisite
|
||||
|
||||
2026-09-25. Selected private prerequisite following bounded design and independent
|
||||
ownership/oracle review; implementation and runtime acceptance pending. Source baseline:
|
||||
`fa7a980418ef99d772e67c66c28dfa7b5181f875`. This implements the output construction
|
||||
and old-owner lifetime portion of the [rename successor](analysis-mixed-rename-plan-contract-20260924.md#mandatory-successor-accepted-rewritten-output).
|
||||
It does not implement accepted output, public source8, persistence or history.
|
||||
The [carried parser prerequisite](analysis-carried-phase-contract-20260925.md)
|
||||
and its remaining before-growth limitations retain their stated scope.
|
||||
The [producer inventory](analysis-mixed-emission-design-20260925/NEXT-PRODUCER-HOOKS.md),
|
||||
[independent critique](analysis-mixed-emission-design-20260925/NEXT-PRODUCER-CRITIQUE.md),
|
||||
[transition proposal](analysis-mixed-emission-design-20260925/EMISSION-TRANSITION-PROPOSAL.md)
|
||||
and [contract review with ownership correction](analysis-mixed-emission-design-20260925/EMISSION-CONTRACT-REVIEW.md)
|
||||
record candidates and rationale. This selected contract supersedes conflicting
|
||||
earlier proposal choices, including the owning enum and runtime byte guard.
|
||||
|
||||
## Ownership and API
|
||||
|
||||
Consume the opaque `RenamePlan` and borrow a complete current `SourceBundle`.
|
||||
Revalidate current entry, complete file set, paths and all bytes against the
|
||||
original snapshot before any copying, including for no-op. Reuse the planner's
|
||||
complete physical edit validation; no external edit list, Project or policy may
|
||||
replace its authority.
|
||||
|
||||
If no names changed, return the original `AuthoredMixedProject` unchanged, with
|
||||
its actual bundle, policy, work usage, warnings and storage. Allocate no output.
|
||||
The changed result owns a complete pending bundle, the exact inherited `Limits`
|
||||
and `ProjectLimits`, its verified output storage total O, the desired Project
|
||||
borrow and scalar original expression-site count E. It retains no old syntax,
|
||||
catalog, warning, index or plan map. Original caller-owned source and desired
|
||||
Project remain untouched. This does not claim those external borrows are freed.
|
||||
|
||||
Keep pending fields private. Expose no bundle extraction, mutation, installation,
|
||||
history, save or accepted-result conversion. Tests may inspect fields. No general
|
||||
constructor accepting arbitrary bundle/Slots/policy is authorized. The only
|
||||
construction path consumes the actual plan after checked emission. Reuse or add
|
||||
a sealed carried-snapshot connection only if it accepts this actual pending
|
||||
owner, never loose parts; invoking parser verification remains outside this slice.
|
||||
|
||||
For changed emission in this prerequisite require original legacy syntax
|
||||
(`carried == None`). Reject an explicit carried source before output reservation,
|
||||
even its borrowed-zero mode. Dropping a source witness only drops its guard; an
|
||||
external RetainedSnapshot would remain alive, so claiming only O survived would
|
||||
hide C. Future repeated owned editing needs an explicitly consuming owner
|
||||
transition or surviving C guard. No-op may preserve any source and its guard
|
||||
unchanged, because no old reservation is discarded. This narrow provenance
|
||||
restriction is intentional, not a numeric-zero shortcut or a final editing API.
|
||||
|
||||
Select two explicit consuming methods:
|
||||
`into_unchanged(self, current) -> Result<AuthoredMixedProject<'s>, Failure>` and
|
||||
`emit_changed_unverified(self, current) -> Result<PendingRename<'t>, Failure>`.
|
||||
Both validate current snapshot and complete edits and enforce their branch;
|
||||
calling the wrong branch refuses. Callers can select using the existing immutable
|
||||
`changed_names().is_empty()` observation. A branch cannot change after that read.
|
||||
This separates source and desired lifetimes without boxing, a large common
|
||||
owning result enum, generic callback machinery or lint exemptions. Do not
|
||||
duplicate full policy inside old syntax owners. Keep all existing stack controls.
|
||||
|
||||
## Storage and construction
|
||||
|
||||
Let B be the existing plan owner's live ledger, which already includes both the
|
||||
R changed-name entries and M physical-edit entries. Do not add R+M twice.
|
||||
Compute the complete final file byte sum S using the existing checked original
|
||||
coordinate sizing walk, including untouched files. Then compute
|
||||
|
||||
```text
|
||||
O.entries = original complete file count F
|
||||
O.occurrences = 0
|
||||
O.text = S + sum(canonical key UTF-8 byte lengths) + entry UTF-8 byte length
|
||||
peak during emission = B + O
|
||||
```
|
||||
|
||||
Every addition/subtraction must be checked. Reserve all O against the original
|
||||
ledger and exact inherited policy before constructing any output String, copied
|
||||
key, entry or map element. Reserving the complete monotone output up front covers
|
||||
its partial construction; capacity/allocator overhead remain outside the logical
|
||||
metric. Original sources and desired replacement aliases are borrowed during
|
||||
copying. No lengths map, copied edit list, temporary complete file or second
|
||||
output buffer is needed. Any additional owned container requires explicit scope
|
||||
and reservation before growth. Source-size shrinkage does not release old B.
|
||||
|
||||
Walk canonical files and the sorted edit stream. For each file, append original
|
||||
untouched ranges and the exact qualified target's desired alias in original byte
|
||||
coordinates, then the tail. Produce one String per file, including untouched
|
||||
files; preserve complete paths and entry. Avoid reparsing, decoding, UUID creation,
|
||||
normalization and global textual replacement. Validate original UTF-8 boundaries
|
||||
and nonoverlap through the existing complete edit validator before construction.
|
||||
Check the constructed length against independently computed size before ownership
|
||||
transition. Check final complete file count and byte total without a second copy.
|
||||
|
||||
Capture policy and E as scalars; destroy old plan maps and the complete old source
|
||||
owner explicitly before returning the pending owner. O is then the only retained
|
||||
in-scope owner from this operation. Failure yields existing located diagnostics
|
||||
and drops partial output; no partial owner escapes. Preserve failure ordering:
|
||||
stale snapshot, invalid edit map/size, output storage reservation, construction
|
||||
consistency; changed carried-source refusal precedes output reservation.
|
||||
Arithmetic fault injection must be labeled as such, not successful
|
||||
real-world owner provenance.
|
||||
|
||||
## Verification and remaining boundary
|
||||
|
||||
Use independent literal original-coordinate maps and expected full bytes for OA
|
||||
and SA, simultaneous global/scoped swaps, repeated/reordered labels, cross-file
|
||||
references, ordinary categories, Unicode/CRLF and comments. Verify copied untouched
|
||||
files and exact entry/keys. No-op must preserve original owner data/pointers, not
|
||||
an equivalent reconstructed owner. Corrupt current touched/untouched data, paths,
|
||||
entry and file set, and corrupt plan edit locations/targets through test-only
|
||||
access; require no result. Reuse valid planner tests where they cover the same
|
||||
validator rather than duplicate all cases.
|
||||
|
||||
Independently hand-count a small real fixture's B and O. Choose exact entry/text
|
||||
caps that admit all predecessor phases and refuse cap-minus-one at emission.
|
||||
Do not mutate an admitted live ledger or widen policy to manufacture this test.
|
||||
Assert final O, original exact policies, desired borrow and E. Include combined
|
||||
grow/shrink and unchanged-file sizing. Small-stack emission/drop checks prepare
|
||||
parser prerequisites on the normal thread, then move the complete consumed plan
|
||||
into the worker; preserve all prior full-pipeline controls and limitations.
|
||||
|
||||
Independent byte fixtures test the emitter; the pending type does not assert
|
||||
runtime semantic authentication. A duplicate streaming emitter/comparator is not
|
||||
required just to return this unverified internal state. Final acceptance still
|
||||
requires actual-output parse/resolve/admission under the exact policy and carried
|
||||
O, strict whole-Project equality with E scratch reserved, a resolved temporal
|
||||
parser accounting contract, and an owning accepted result after temporary syntax
|
||||
drops. Those requirements cannot be substituted by length checks or byte tests.
|
||||
Original E is provenance metadata, not a demonstrated bound on arbitrary faulty
|
||||
emitted syntax. The later verifier must justify its equality bound against both
|
||||
actual operands and cannot rely on an unperformed runtime topology check.
|
||||
@@ -0,0 +1 @@
|
||||
*.md -text
|
||||
@@ -0,0 +1,43 @@
|
||||
# Independent emission contract review
|
||||
|
||||
Reviewed `harness/analysis-mixed-emission-contract-20260925.md` against the current `fa7a980418ef99d772e67c66c28dfa7b5181f875` planner/guard shapes, accepted rename successor and carried-phase scope. Read-only; no contract/source edits, builds or network activity.
|
||||
|
||||
Disposition: **no blocking design finding; suitable for the bounded private implementation described.** This review accepts its unverified prerequisite boundary, not final edited-output acceptance.
|
||||
|
||||
## Runtime byte guard decision
|
||||
|
||||
The selected omission is reasonable. My earlier NEXT-PRODUCER-CRITIQUE recommended a streaming guard partly to establish authenticated topology for a potential later parser envelope. This draft does not grant that authority. An opaque unverified pending owner does not need a second runtime comparator merely to prove the same emission operation again. Independently authored complete byte fixtures discriminate emitter defects without installing another production algorithm.
|
||||
|
||||
A runtime guard becomes worthwhile if a later phase relies on unchanged token/AST/include topology **before** parsing actual bytes, or promises runtime preservation of all non-name bytes independently of emitter correctness. In that case it should consume actual output by matching borrowed original/replacement chunks and prefixes, rather than reconstructing output through the same append loop/helper; comparison must separately cover entry, file set, keys and untouched files. Whole-Project equality alone does not establish lexical preservation of comments/formatting. None of those stronger claims is made by this pending type, so the guard can remain deferred.
|
||||
|
||||
## Ownership and sizing
|
||||
|
||||
The contract correctly requires stale complete-snapshot and complete-edit validation before any output copies, including no-op. No-op returns the original authored owner and allocates no output. Changed output derives solely from the consumed sealed plan, borrows aliases while copying, retains exact policy and desired borrow, and offers no extraction/mutation/save/adoption escape.
|
||||
|
||||
O is correctly F entries plus every final source byte, copied canonical key and the distinct entry-path bytes, with zero lower occurrences. B already contains R+M. Reserving B+O before construction covers monotone partial output without double-charging plan maps or prematurely crediting shrinkage. Repeating a scalar sizing walk is preferable to an extra lengths map. Per-file length, complete count and total consistency checks detect size-construction disagreement without implying byte authentication.
|
||||
|
||||
The named old-owner drop is essential: capturing output/policy/desired/E does not itself release old syntax/index/warnings/maps. Implementer must make that destruction precede return and preserve the original external source/desired borrows. Failure diagnostics retain the existing bounded rejection exception; no partial pending owner escapes.
|
||||
|
||||
The proposed callback or nested-result shape is appropriately left for actual type review. It should not evade accounting with an otherwise unnecessary Box, duplicate the full policy inside old syntax or change expectations merely to satisfy enum-size lint. Compiler/Clippy and the inherited stack controls remain necessary evidence.
|
||||
|
||||
## Useful acceptance boundaries
|
||||
|
||||
The independent full-byte fixtures, combined growth/shrink, copied untouched files and no-op ownership assertions test the actual new behavior. Reusing the existing complete edit validator and its corruption evidence avoids duplicating the entire planner test matrix. New B+O exact-cap tests must admit predecessor phases under the same profile; final O assertions must follow actual old-owner destruction rather than scalar subtraction alone.
|
||||
|
||||
Normal-thread setup followed by scoped small-stack emission/drop isolates the new operation without falsely promising that upstream parsing fits every worker stack. Existing full-pipeline controls and prior abort limitations remain separate.
|
||||
|
||||
## One explicit future-use caution
|
||||
|
||||
Original E is valid provenance metadata, but without runtime byte authentication it is **not automatically a bound on arbitrary actual output topology**. A faulty emitter may produce admissible-but-wrong bytes with a different structure. A future topology-based parser envelope or whole-Project comparison must prove its scratch bound for that case, derive a safe bound from admitted actual output/intent, or reject structural mismatch before exceeding the reserved bound. Merely retaining E from the original owner is not that proof.
|
||||
|
||||
This is not a blocker to pending emission: the draft defers parser verification, temporal accounting and strict whole-intent equality. Preserve that deferral in code/API/docs; do not wire a future accepted-result constructor while relying only on this review, output lengths or test fixtures. No broader parser scratch expansion is required to implement the selected private milestone.
|
||||
|
||||
## Qualification after carried-lifetime finding
|
||||
|
||||
The original no-blocker disposition missed a material ownership edge identified by the other oracle reviewer. A source parsed through a retained carried guard borrows an external RetainedSnapshot. Destroying its syntax/index/plan drops only the guard, not that external owner or C. Returning a changed pending result with only O would therefore discard live-storage accounting authority without ending the corresponding ownership. My original disposition is insufficient without the following amendment.
|
||||
|
||||
Reread the amended contract: **changed emission requires `syntax.carried == None`; any Some variant, including borrowed-zero mode, is refused before O reservation.** This is an explicit provenance boundary, not `slots == 0`. No-op remains permitted for any source only because it returns the same authored owner and guard without releasing the original baseline. Complete-current-snapshot and edit validation still precede copying/no-op as specified.
|
||||
|
||||
With this restriction, the pending operation can honestly retain only O after the consumed legacy syntax/index/plan owner dies. Future repeated edits of owned output require consuming the actual external output owner or retaining its guard and counting surviving C; merely borrowing and later dropping a source witness is inadequate. Runtime tests should distinguish changed legacy success, changed borrowed-zero refusal, changed retained refusal and guarded no-op preservation, with adequate caps so unrelated resource failures do not hide this rule.
|
||||
|
||||
The amendment resolves the identified edge for this prerequisite; the qualified disposition is suitable for bounded implementation. I also verified the contract now explicitly states that original E does not bound arbitrary faulty emitted syntax and requires a justified bound against both future equality operands.
|
||||
@@ -0,0 +1,133 @@
|
||||
# Proposed independent emission-transition oracle
|
||||
|
||||
2026-09-25. **Design proposal and source-derived expectations, not runtime results or accepted output.** Source: `fa7a980418ef99d772e67c66c28dfa7b5181f875`. Reviewed initial `EMISSION-TRANSITION-PROPOSAL.md` SHA256 `c0734464991ab841ae59d6c3c26a5095b3c0908ce35656158db92476d76682de`; parent subsequently selected changed-source legacy-only guard admission. No application parser, emitter, Cargo or executable was run. UTF-8 lengths below are literal byte arithmetic, not values obtained from implementation output.
|
||||
|
||||
Use A for the admitted authored source ledger, L for the plan's ledger **already including R+M**, and O for newly owned output. Emission must reserve L+O, not L+(R+M)+O. All examples below start through the legacy source path, so no external carried baseline is concealed in A.
|
||||
|
||||
## Minimal exact predecessor and emission caps
|
||||
|
||||
E is the existing `project::tests::single("")` literal, entry/key `main.arcadia` (12 UTF-8 bytes each):
|
||||
|
||||
```text
|
||||
arcadia 8;\nproject p "Project" id "00000000-0000-0000-0000-000000000001" {\n\n}\n
|
||||
```
|
||||
|
||||
This notation represents actual LF bytes. Original source length78, syntax U1/M11, retained syntax text8; no expressions, references, lower occurrences or warnings. These grammar/accounting terms also appear in the independent carried-phase oracle. Desired changes only the project alias from `p` to `next`. Exactly one physical token changes: canonical file0, range19..20. Independently authored complete expected source is:
|
||||
|
||||
```text
|
||||
arcadia 8;\nproject next "Project" id "00000000-0000-0000-0000-000000000001" {\n\n}\n
|
||||
```
|
||||
|
||||
Expected source length81. The extra entry-path string is separate from the equal-spelled file key. Thus O=(1,0,81+12+12)=(1,0,105).
|
||||
|
||||
| E phase | Retained entries/text after phase | Maximum entries/text within phase |
|
||||
| --- | --- | --- |
|
||||
| Resolve | 12 / 8 | 16 / 10 |
|
||||
| Typed index (one global project name, no typed sites) | 13 / 9 | 14 / 9 |
|
||||
| Ordinary index (one project header) | A=14 / 9 | 15 / 9 |
|
||||
| Rename E0, Q1, R1, M1 | L=16 / 9 | 16 / 9 |
|
||||
| Output reservation | L+O=17 / 114 | 17 / 114 |
|
||||
| Pending after old source/maps drop | O=1 / 105 | — |
|
||||
|
||||
All occurrence counts are zero. Phase maxima are coordinatewise logical maxima, not allocator/RSS measurements. The complete real path must fit at **entries17 / bytes114**. Entries16 with bytes114, or bytes113 with entries17, clears all predecessors and independently refuses the upfront emission reservation. Source-byte work78, planned output81 and changed-name-byte check4 all fit either byte cap. No live Budget mutation is needed to manufacture these pairs.
|
||||
|
||||
Source derivation: `mixed/project/accounting.rs:233–352` charges U+M and syntax text; `mixed/project.rs:273–314` project catalog adds four temporary entries and two alias bytes, later released. `index.rs:288–298` retains one name entry/byte and `:684–709` temporarily reserves the one global lookup. `ordinary_index.rs:98–108,123–153,251–263` retains the single ordinary header while releasing one reverse-global scratch entry. `rename.rs:14–28,30–91,106–120` gives E0 and max(R+Q,R+M)=2; no new copied text is retained by the plan.
|
||||
|
||||
H is the exact three-file Unicode/CRLF chain from `CARRIED-PHASE-ORACLE.md`: main101B includes part25B, which includes last6B (`//é\r\n`); entry `main.arcadia`; all three keys total36B, entry12B. Only change project p→next, again main range19..20; canonical file order last/main/part makes that file1. Main becomes104B; part and last remain byte-exact. Original U3/M19 and syntax text32 give:
|
||||
|
||||
| H quantity | Entries | Occurrences | Text |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| Resolved retained | 22 | 0 | 32 |
|
||||
| Authored A (one name plus project header) | 24 | 0 | 33 |
|
||||
| Planned L, R1/M1 | 26 | 0 | 33 |
|
||||
| Output O, S135 + keys36 + entry12 | 3 | 0 | 183 |
|
||||
| Emission overlap L+O | 29 | 0 | 216 |
|
||||
|
||||
Resolver maximum26entries/34text; typed-index maximum24entries/33text; ordinary-index maximum25entries/33text; planning maximum26entries/33text. Thus **29 / 216** fits the entire path and **28 / 216**, **29 / 215** independently fail only at emission overlap. This distinguishes accounting for all untouched files and copied path/entry strings from source-size-only checks.
|
||||
|
||||
## Unchanged branch and guard boundary
|
||||
|
||||
E no-op fits the original pipeline at entries16/bytes78; H at entries26/bytes132. An unnecessary output copy would exceed those byte ceilings (E A.text+102=111; H A.text+180=213). A successful unchanged branch at these actual inherited caps therefore discriminates against reserving/copying identical output. It must return the original authored owner with unchanged ledger, Usage/numeric work, warnings, Project, maps and exact original bundle borrow. Signed-zero-equivalent target intent is also unchanged bytes; no new semantic rewrite is authorized.
|
||||
|
||||
Freshness is checked even on unchanged intent: mutate an untouched comment byte, entry, key or file set in an independent current snapshot and require refusal before branch return. An independently allocated but equal complete current bundle is acceptable; emission authority still belongs to the retained original, not that comparison input.
|
||||
|
||||
**Required selected edge test:** produce an authored source via carried parsing, both borrowed-zero Some guard and actual retained nonzero guard, then plan a changed alias. Changed emission must refuse before O reservation for either Some guard. No-op must preserve and return that original guarded owner. Dropping a borrowing guard does not drop its external RetainedSnapshot; carrying only O after such a changed source would hide still-live counted ownership. The narrow legacy-None restriction avoids that false transition without inventing an emitter/verification ownership architecture. A zero numeric C is not sufficient to bypass the selected guard restriction.
|
||||
|
||||
## Independent four-file mixed swap bytes
|
||||
|
||||
This proposed fixture combines a typed/ordinary global swap with cross-family references, a leading multibyte character, CRLF, comments containing alias spellings and an untouched included file. Every line below ends CRLF, including the last line of each file. UUID strings are literal. Entry is `main.arcadia`; canonical order is a/b/main/untouched.
|
||||
|
||||
Original `a.arcadia`:
|
||||
|
||||
```text
|
||||
calculation f "F" id "00000000-0000-0000-0000-000000000010" { result Number; body 1; }
|
||||
```
|
||||
|
||||
Original `b.arcadia`:
|
||||
|
||||
```text
|
||||
// f and w are comments
|
||||
oa_activity w "Work" id "00000000-0000-0000-0000-000000000002" {}
|
||||
```
|
||||
|
||||
Original `main.arcadia`:
|
||||
|
||||
```text
|
||||
// 雨
|
||||
arcadia 8;
|
||||
project p "Project" id "00000000-0000-0000-0000-000000000001" {
|
||||
include "a.arcadia"; include "b.arcadia"; include "untouched.arcadia";
|
||||
oa_property v "Value" id "00000000-0000-0000-0000-000000000003" { subject @w; type Number; base @f(); }
|
||||
}
|
||||
```
|
||||
|
||||
Original and expected `untouched.arcadia`:
|
||||
|
||||
```text
|
||||
// 雪 untouched f w
|
||||
```
|
||||
|
||||
Desired changes exactly calculation10 alias f→w and activity2 alias w→f; every other Project field remains original. Independently authored expected `a.arcadia`:
|
||||
|
||||
```text
|
||||
calculation w "F" id "00000000-0000-0000-0000-000000000010" { result Number; body 1; }
|
||||
```
|
||||
|
||||
Expected `b.arcadia`:
|
||||
|
||||
```text
|
||||
// f and w are comments
|
||||
oa_activity f "Work" id "00000000-0000-0000-0000-000000000002" {}
|
||||
```
|
||||
|
||||
Expected `main.arcadia`:
|
||||
|
||||
```text
|
||||
// 雨
|
||||
arcadia 8;
|
||||
project p "Project" id "00000000-0000-0000-0000-000000000001" {
|
||||
include "a.arcadia"; include "b.arcadia"; include "untouched.arcadia";
|
||||
oa_property v "Value" id "00000000-0000-0000-0000-000000000003" { subject @f; type Number; base @w(); }
|
||||
}
|
||||
```
|
||||
|
||||
Exact original-coordinate edit oracle:
|
||||
|
||||
| Canonical file | Range | Old | New | Target |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 0, a.arcadia | 12..13 | f | w | calculation10 |
|
||||
| 1, b.arcadia | 37..38 | w | f | activity2 |
|
||||
| 2, main.arcadia | 232..233 | w | f | activity2 |
|
||||
| 2, main.arcadia | 254..255 | f | w | calculation10 |
|
||||
|
||||
File byte lengths are88,92,265,22, unchanged by the swap; S467, key bytes47, entry12. Therefore **O=(4,0,526)**, R2/M4. The original raw-expression count E is2 (calculation literal and property call). Preserve this scalar in pending, but do not call it an already-proven bound for future equality over arbitrary reparsed output; that successor must establish the compared side/shape bound explicitly. The cap proof for this richer fixture is intentionally not invented: use generous inherited limits and the independent E/H cap pairs above. Fixture syntax/admission still requires a real gate; these bytes do not report an unrun parser success.
|
||||
|
||||
This exact output catches sequential spelling substitution (f→w followed by w→f), alias replacement in comments/display names, loss of markers, accidental newline conversion and omission of untouched content. Add the existing independently authored rich OA/SA full maps for scoped parameter swaps/reordered labels and the category fixture for comment-separated references/historical UUID preservation. Do not generate expected output by running the implementation's sizing/emission helper or copying its returned map.
|
||||
|
||||
## Outcome shape and remaining evaluator requirements
|
||||
|
||||
The proposed owning enum is understandable but contains the large unchanged authored owner; smaller pending payload does not make that union small. Box/Rc/Arc would introduce allocation and accounting concerns rather than demonstrate a free repair. Source-only analysis cannot guarantee debug-frame behavior, particularly after the prior inline-policy regression.
|
||||
|
||||
Parent selected **two consuming, branch-checked methods**: `into_unchanged(self,current) -> Result<AuthoredMixedProject<'s>,Failure>` and `emit_changed_unverified(self,current) -> Result<PendingRename<'t>,Failure>`. Existing `changed_names().is_empty()` supports caller selection; both methods independently validate freshness and enforce the selected branch. Wrong-branch use refuses with no partial output. This avoids a new large owning enum, generic callbacks, extra owner allocation and lint suppression; pending has no original-source lifetime and unchanged has no desired lifetime. Tradeoff: two explicit private entry points. Scoped callbacks were considered as an alternative if one dispatch method were essential, but add generic callback layering. The selected split still requires stack verification and does not warrant weakening the old 128 KiB tests.
|
||||
|
||||
Before implementation acceptance require: complete expected maps, exact source/key/entry count and O retention; E/H predecessor-clearing caps; stale/no-op and Some-guard controls; immutable desired/old bundles; policy equality across all captured fields; no parse/Exact work during emission; no pending partial result on checked failure; and an observable source/map drop before pending escapes. A helper returning Pending from a scope holding the original bundle should prove pending does not borrow old source after the changed transition. After extracting unchanged, desired need not remain borrowed. Keep all legacy stack controls unchanged and add success/no-op/refusal/drop coverage for the chosen new outcome shape. No production pending extraction/adoption, parser entry, final whole-model equality or accepted-output claim is part of this oracle.
|
||||
@@ -0,0 +1,105 @@
|
||||
# Private emitted-unverified rename transition — proposal
|
||||
|
||||
2026-09-25. **Design only; not an accepted contract, implementation or edited-output acceptance.** Source pin `fa7a980418ef99d772e67c66c28dfa7b5181f875`. Paths below are relative to `crates/oa-core/src/`. No source changes, builds or Gitea operations.
|
||||
|
||||
## Minimal outcome and privacy
|
||||
|
||||
Parent selection supersedes the earlier enum/callback candidates: use two named consuming methods, with callers selecting from immutable changed_names().is_empty() before consumption. Both methods call validate_current and the existing validate internally, then reject the wrong branch. No common result enum, callback adapter, heap indirection or lint suppression is needed. This is accepted design direction, pending implementation/runtime gates.
|
||||
|
||||
Use a narrow `rename::emission` child owning the transition and an opaque pending owner:
|
||||
|
||||
```rust
|
||||
// Fields private to rename::emission. Not Clone/Copy/Serialize/AsRef/Deref.
|
||||
struct PendingRename<'t> {
|
||||
bundle: SourceBundle,
|
||||
limits: Limits,
|
||||
project_limits: ProjectLimits,
|
||||
retained: Slots,
|
||||
expression_nodes: usize, // E, verified original count for later equality
|
||||
desired: &'t Project,
|
||||
}
|
||||
|
||||
impl<'s, 't> RenamePlan<'s, 't> {
|
||||
fn into_unchanged(
|
||||
self, current: &SourceBundle,
|
||||
) -> Result<AuthoredMixedProject<'s>, Failure>;
|
||||
|
||||
fn emit_changed_unverified(
|
||||
self, current: &SourceBundle,
|
||||
) -> Result<PendingRename<'t>, Failure>;
|
||||
}
|
||||
```
|
||||
|
||||
Use the repository's existing restricted visibility (`pub(in crate::language::analysis_source)`) only where the private caller must name types/methods. Implementation in a child of `rename` can read its parent's private plan fields; no mutable plan/model/token authority is exposed. `current` is only a freshness comparison input; emission reads the retained original bundle. It cannot substitute the source to emit.
|
||||
|
||||
For **this bounded step**, PendingRename owns the bundle/policy/Slots/E directly. Do not widen `carried::RetainedSnapshot` with a sibling-callable `new(bundle, slots, policy)`. The future sole consuming transition can take an actual sealed PendingRename and transfer its verified fields into RetainedSnapshot after its execution contract is accepted. Until then the pending type exposes no production bundle getter/extractor, no borrowed carried input, no verification/adoption method and no conversion to SourceBundle/Project/session state. `cfg(test)` read-only inspection suffices to verify emitted bytes/counts/intent/policy. Existing RetainedSnapshot fixture constructors and carried parse APIs remain unchanged.
|
||||
|
||||
This is an explicit staging choice: the current result is an **emitted-unverified owner**, not yet a RetainedSnapshot usable by production carried parsing. Its retained formula is exactly the future snapshot formula; eventual transfer can move fields without copying. If parent instead requires constructing RetainedSnapshot in this slice, the alternative is a carried-owned factory accepting the **sealed RenamePlan itself** and performing the emission transition internally, with minimal read-only/consuming plan access. An arbitrary-parts constructor is not justified. The pending-only option changes fewer existing privacy boundaries and creates no premature verification entry.
|
||||
|
||||
An owning pending result with inspection only in fixtures is sufficient for an independently testable producer: consumers can hold/drop it but cannot save, install or treat its raw bytes as accepted model state through its API. The private name and absence of extraction are intentional authority restrictions, not runtime cryptographic authentication. A future trusted private implementation can add its sole verifier; no hash/token duplication is needed now.
|
||||
|
||||
## No-op and unchanged authority
|
||||
|
||||
First run existing `validate_current(current)` (`rename.rs:161–169`) so even no-op execution refuses a stale complete snapshot. If the sealed plan's names/edits are empty, consume its empty maps and return the original AuthoredMixedProject unchanged. No output String/key/entry copies, new snapshot parsing or new storage reservation occur. The source's original exact bundle borrow, maps, warnings, Project and policy survive.
|
||||
|
||||
The unchanged owner remains an already-admitted original owner, not a newly emitted result. The whole-target gate already established alias-only equality; signed-zero-equivalent desired representation does not authorize rewriting scalar bytes. The no-op branch can end the desired borrow because it does not need later output equality.
|
||||
|
||||
Do not implement a no-op by allocating an identical owned SourceBundle and calling that free. Do not run a second whole-model comparator merely to reconfirm the sealed plan.
|
||||
|
||||
## Validation and emission sequence
|
||||
|
||||
1. Consume the opaque plan. Complete-current-bundle validation compares entry, all keys, exact file set and every byte, including untouched files.
|
||||
2. Rely on plan construction's `validate()` proof for whole token membership, original spelling, UTF-8 ranges and nonoverlap (`rename.rs:178–239`). No API mutates the maps afterward. Reuse its sizing logic and source authority; do **not** re-lex/lower/admit or build another seen/token/map inventory. Calling the same private validator once at emission can support internal fault-injection tests, but is redundant for sealed production authority and is not required as a new authentication layer. Parent should choose deliberately; prefer the sealed proof and cheap checked emission invariants below.
|
||||
3. If changed, reject every explicit carried source (syntax.carried.is_some()), including borrowed-zero mode, before output reservation. Its external retained owner would survive dropping the source guard; O alone would then omit live C. No-op preserves any carried guard without rebasing. For this prerequisite changed emission therefore starts only from legacy syntax. Then compute checked complete output length S using the existing subtract-removed-before-add-replacements formula (`rename.rs:261–308`). Include every original file. Sum copied canonical key byte lengths K and entry byte length J using checked arithmetic; let F be the existing file count. Capture exact Budget::limits, syntax ProjectLimits, desired borrow and E as scalar metadata, without cloning the model or names.
|
||||
4. Reserve the entire output increment `O=(F,0,S+K+J)` against the **still-live plan ledger** before allocating/copying any output String, map key or entry. The current ledger already includes old B plus R+M; do not add those again. Failure maps through existing source diagnostics while the old witness is alive and returns no pending owner.
|
||||
5. Emit each file exactly once into one final String from original coordinates, traversing canonical files and the sorted physical edit map. Repeated sizing iteration is allowed; a new lengths map is unnecessary. For each file compute its final length with a scalar pass over that file's edits, allocate that String capacity, then append unchanged original slices and replacement names in order, followed by the untouched suffix. Clone that canonical path once into the final map. Files with no edits still get one exact full copy. Clone entry once. Markers/comments/includes/prose/UUIDs remain in unchanged slices.
|
||||
6. Every cursor/range calculation is checked; exact token boundaries are inherited from the plan. At each file require produced logical length equal its computed length, final cursor within source and all file edits consumed. Require complete file count and total S after assembly. These are inexpensive producer invariants, not a new independent syntax authenticator or guarantee of intended semantic validity.
|
||||
7. No partial map escapes on any failure. After complete assembly, snapshot the surviving O/policy/E/desired scalars, destructure and explicitly drop the old names/edit maps and AuthoredMixedProject **before constructing and returning PendingRename**. The output bundle moves once into pending. Drop the old ledger with its owner; pending retained state is O, not B+R+M+O.
|
||||
|
||||
Rust's return/drop order should be visible in code: use a narrow local owning output, capture scalar/reference metadata, `drop(names); drop(edits); drop(source);` then construct pending. Borrowed original SourceBundle remains caller-owned after the source auxiliary owner drops, exactly as before; this transition does not claim to free or charge that external borrowed input. The external desired Project also remains borrowed and unchanged.
|
||||
|
||||
## Counts and lifetimes
|
||||
|
||||
Let B be the admitted authored owner's existing live auxiliary Slots, R changed targets and M changed physical edits. The plan already retains `L=B+(R+M,0,0)`. During emission the output reservation establishes peak **L+O** in each existing dimension:
|
||||
|
||||
```
|
||||
O.entries = F
|
||||
O.occurrences = 0
|
||||
O.text = S + sum(canonical_path_utf8_bytes) + entry_utf8_bytes
|
||||
```
|
||||
|
||||
The entry is a separately owned String even when it equals a map key. Unchanged files still contribute their complete output bytes to S. Old source Strings are externally borrowed, not copied into B; all newly owned output bytes are counted once. Existing copied old syntax/name/warning text remains live in L throughout production and cannot be released early merely because a replacement shrinks a file.
|
||||
|
||||
A whole-output upfront reservation avoids per-file partial reservation bookkeeping and an extra lengths map. Final file capacity uses its exact precomputed length, but accounting is logical bytes, not allocator capacity/BTree node overhead/RSS. Temporary String then map insertion transfers the same reservation, not two copies. Checked reserve before creation does not promise recovery from operating-system allocator failure; existing logical resource-failure behavior remains the scope.
|
||||
|
||||
Once the old owner/maps drop, pending stores O and the exact inherited policy, with E retained only as original expression-count provenance for a future whole-Project equality reservation; it is not a proven bound on arbitrary faulty emitted syntax. Future carried verification must still account O plus parser/resolver allocations and decide required producer timing; the successful emission reservation proves none of those later checks. It also does not prove output is syntactically admissible or semantically equal to desired.
|
||||
|
||||
## Avoiding another stack-layout regression
|
||||
|
||||
Do not add Box/Rc/Arc or clone the old owner/model to simplify the outcome. Such allocation would require its own design/accounting and is unnecessary. Enum size is the maximum variant payload plus tag/alignment, not the sum of both payloads; nevertheless debug return temporaries can amplify a large unchanged owner. Keep helper layering shallow and do not retain duplicate inline policy in witnesses that already own it. Pending needs its one policy because the old owner actually drops.
|
||||
|
||||
The selected split consuming API avoids a common large enum and requires no callback framework. Each method enforces its branch; the two explicit wrong-branch errors are acceptable bounded API checks. Ordinary Result layouts and generated stack usage still require parent gates; this source-level choice is not a measured stack guarantee.
|
||||
|
||||
## Independent acceptance cases
|
||||
|
||||
- **No-op:** pointer identity of original bundle and retained owner state, identical Slots/Usage/numeric/warnings, no output allocation/copy; stale unchanged-file or entry mismatch still refuses. No desired mutation.
|
||||
- **Whole bytes:** rich OA/SA global+scoped swaps, nested/reordered/repeated labels, canonical multi-file coincident spans, Unicode/CRLF and category fixture with line-comment-separated references. Independently authored entire expected file strings/maps, not reconstruction through an emitter-like expected helper. Every non-token byte must match original at its corresponding unchanged segments; historical UUIDs/prose untouched.
|
||||
- **Combined sizing:** the existing31-byte node shrink plus typed f growth example emits924 bytes from930, despite a hypothetical typed-only954; the933-byte negative refuses the inherited930-byte output-size cap before copying. Owned-text overlap may legitimately refuse under that same cap even when final S fits: use sufficient inherited text allowance for a successful emission test and distinguish planning-size versus live-owned-text caps.
|
||||
- **Exact overlap:** independently hand-count F/S/K/J and old plan L. A cap fitting L and O separately but below L+O refuses before the first output copy. Exact combined cap succeeds if all actual predecessors fit; test entries and text independently. Assert final pending retained exactly O, not L+O or O twice. Do not tighten a live owner's Budget after admission to manufacture a boundary.
|
||||
- **Complete stale input:** touched and untouched source bytes, file removal/addition, path rename and entry changes reject before allocation. Equal independent current bundle content succeeds; emitted authority still comes from the retained original.
|
||||
- **Failure lifecycle:** malformed internal fixtures must enter existing plan-validation tests before a sealed plan exists; do not fabricate externally installable edited maps. Where emission has its own checked-arithmetic/range invariants, targeted private fault injection can assert no partial pending owner escapes. Overflow beyond valid profile ceilings is explicitly labeled helper/fault-injection evidence.
|
||||
- **Provenance limitation:** changed sources from both real retained C and explicit borrowed-zero carried mode refuse before output allocation; no-op preserves each original guard. No numerical zero bypass. A future consuming carried-owner transition is required before repeated owned edits.
|
||||
- **Ownership/API:** desired remains externally borrowed, old owner is dropped before Pending creation, output is independently owned and immutable through available API; only cfg(test) inspections reveal bytes/policy/O/E. No production extraction, parse/admission, session adoption or persistence path is added.
|
||||
- **Regressions:** preserve all existing planner/carried outcomes and unchanged small-stack controls, inherited failure dispositions and formatting exclusions. Emitted-unverified success is reported separately from future output acceptance.
|
||||
|
||||
## Minimal files and next decisions
|
||||
|
||||
- `mixed/project/ordinary_index/rename/emission.rs`: emission outcome, pending owner, sole consumed-plan producer and its tests as a child module. Access existing private plan/source fields through lexical ancestry, not public mutable accessors.
|
||||
- `rename.rs`: module wiring and, if needed, narrowly factor existing per-file checked-length iteration for use by sizing/emission without allocating a lengths table. Preserve the sealed constructor validation and existing inspection APIs.
|
||||
- Scoped implementation note/tests. `mixed/carried.rs` need not change for pending-only delivery; no arbitrary RetainedSnapshot constructor is opened.
|
||||
|
||||
Read against the parent draft harness/analysis-mixed-emission-contract-20260925.md: parent selected pending-only staging, split consuming methods, changed legacy-only provenance, one upfront O reservation and no production extraction. The draft requests reuse of complete edit validation and malformed-plan fixture coverage; invoking the existing validate once before construction is compatible and avoids inventing a second authenticator. No duplicate streaming byte comparator is proposed. Parent retains API selection and contract acceptance authority. A later accepted contract must specify the sole Pending-to-RetainedSnapshot/verifier transition and cannot mistake this owner for accepted rewritten output.
|
||||
|
||||
Minimal independent oracle supplied by reviewer: empty project p renamed next has authored A=(14,0,9), plan L=(16,0,9), output O=(1,0,105), peak L+O=(17,0,114). Exact entries 17/text 114 and predecessor-compatible minus-one caps are the preferred first actual-path acceptance cases. These are independent hand counts, not new runtime results.
|
||||
|
||||
Superseding parent decision: both split methods rerun the existing complete validator after stale-current validation, enforce the correct no-op/changed branch, and refuse changed carried input before O allocation. Earlier proposal preferences for relying only on construction proof or callback delivery are not the implementation selection. Await parent claim/contract publication before source edits.
|
||||
@@ -0,0 +1,60 @@
|
||||
# Independent critique of the next producer proposal
|
||||
|
||||
Read-only review against source `fa7a980418ef99d772e67c66c28dfa7b5181f875`, `NEXT-PRODUCER-HOOKS.md`, the accepted carried-phase contract, the rename-plan successor requirements, Project resource contract and execution domain audit. No source changes, Cargo, network or runtime execution.
|
||||
|
||||
## Recommendation
|
||||
|
||||
Do not make the proposed complete U/M/T producer ledger plus selected parser scratch the next single implementation milestone. The inventory is useful and identifies real timing gaps, but spans the shared scanner, ordinary parser/projection, typed cursor/declarations/expressions, traversal and resolver handoff. Calling that a small set of hooks understates its policy, lifetime and regression surface.
|
||||
|
||||
The smallest useful next rename implementation is **private emission and authenticated output ownership**, stopping before accepted-output authority. Reserve exact output ownership while the old plan is live, emit from original coordinates, independently authenticate actual bytes against the authorized edit stream, and establish a constructor-controlled output owner with exact policy and a documented old-owner drop. Preserve an explicit unchanged outcome for no-op intent. This is necessary work under every plausible future parser accounting decision; it makes actual rename progress without silently settling that decision.
|
||||
|
||||
Do not expose installation/save/history adoption or label this result accepted edited source. The proposed milestone should be explicitly approved as a private prerequisite, just as carried-phase accounting was. A complete accepted-output API still requires actual-byte parse/resolve/admission, strict whole-Project equality with reserved E scratch and a resolved temporal accounting contract.
|
||||
|
||||
## What the contracts actually require
|
||||
|
||||
Three categories should remain separate:
|
||||
|
||||
1. **Necessary new output ownership.** The rename-plan successor explicitly names F output-map entries, complete strings, canonical copied keys and entry; output overlaps the old B during emission. Old syntax/index/plan allocations must actually drop before their reservations disappear, or both owners remain charged. These requirements are unambiguous and cannot be deferred behind a generic parser exclusion.
|
||||
2. **Existing retained inventory.** Project §3 requires before-allocation aggregate checks for in-scope representations. Existing retained syntax aliases, field keys, AST text and projected copies are counted today, but some only at resolution. The domain audit and carried contract explicitly preserve this timing limitation, without waiving the stronger obligation for final execution. Producer hooks for those exact existing families are a defensible way to close it; a proven conservative reservation is another. The successful phase prerequisite does not itself discharge it.
|
||||
3. **Explicit temporaries versus inferred universal scratch.** New emitter containers and already named accounting/equality work must be admitted. The original successor does not separately list every inherited traversal/path/scanner/duplicate-set allocation. Project §3's broad wording and parser proposal's temporary wording deserve an explicit finite scope interpretation; they do not by themselves establish that all of those structures already belong to one exact slot inventory. Automatically adding all of them is a metric expansion, not merely repairing the timing of existing T. Automatically exempting all of them would also be unjustified.
|
||||
|
||||
Accordingly, broad parser scratch expansion is **not established as a prerequisite to implementing private emission**, and should not be imposed implicitly. It may become necessary for the final selected guarantee if parent explicitly includes those structures. That decision should identify each family and its logical unit, rather than using either “everything is already bounded” or “all parser allocation must now be metered.” Allocator capacity, model/serde-conversion heaps, raw validator Issues, BigInt limbs and native stack remain distinct exclusions/limits as documented.
|
||||
|
||||
## Concrete bounded next implementation
|
||||
|
||||
Use the existing opaque RenamePlan and its complete snapshot/edit validation. Add one private emission module beside planning; do not change shared parser producers in this milestone.
|
||||
|
||||
1. Revalidate complete original bundle authority and all physical edits before copying. Preserve canonical paths/file set/entry and both occurrence families. No caller-supplied partial file list or replacement model authority.
|
||||
2. Repeat the existing checked per-file sizing walk, without a lengths map. Compute exact O: F entries and `sum(final source lengths) + sum(copied canonical key byte lengths) + entry byte length`, zero lower occurrences. The existing resolved ledger already includes retained R+M plan maps: reserve O against that live total before constructing any output buffer/key/map. Reserving all O up front is simple and sufficient for a once-built complete bundle whose logical live output grows monotonically toward O. Do not debit old storage for shrinkage.
|
||||
3. Construct each final file once by appending original untouched slices and borrowed desired aliases in increasing original coordinates. Copy untouched files too when delivering the specified complete owned output. Charge every implementation-added temporary if one is introduced; the preferred walk requires only iterator/scalar state and the currently growing output String. Container capacity is not a new logical-byte charge.
|
||||
4. While the original owner still lives, use a separate streaming comparison of the **actual** output against the original-slice/replacement sequence. Check entry, exact canonical file set and every byte, including untouched files. No second output buffer, lexer or output hash as a substitute. This catches admissible-but-wrong non-name damage and creates useful authenticated topology evidence for any later envelope option.
|
||||
5. Capture exact immutable policy and E scalar while the old witness still authorizes them. Consume/drop old syntax, indexes, warnings and plan maps at a named transition; retain only the complete output owner, desired borrow, policy and documented scalar metadata. Install O through the sole private output transition, never a general `new(bundle, arbitrary_slots)` constructor. The stable owner may later lend bytes through the carried guard. Do not return a self-borrowing bundle-plus-syntax pair.
|
||||
6. For no-op intent, return the original owner in an explicit unchanged branch without constructing/copying a complete output. No history/install behavior follows from this private result.
|
||||
|
||||
The output handle should be opaque and have no public extraction/adoption route. Test-only inspection can examine bytes and provenance. A later internal scoped verifier can borrow it and drop temporary syntax before returning owning acceptance. Failure drops partial output and consumes no install authority; the immutable original caller-owned source bytes and desired model remain unchanged.
|
||||
|
||||
This is a proposal requiring a small successor contract, not authorization to add a production constructor under the already-finished phase-only contract.
|
||||
|
||||
## Acceptance for that bounded milestone
|
||||
|
||||
Reuse the independent complete rich OA/SA, swaps, scoped repeated/reordered call labels, cross-file Unicode/CRLF and ordinary-category maps. Author exact expected bytes from the original fixture and literal replacement coordinates, without invoking the emitter as its own oracle. Include untouched files and the previously agreed combined grow/shrink sizing case. Verify original bytes/intent unchanged and no-op preserves ownership.
|
||||
|
||||
Hand-count emission cap/cap-minus-one at **old live B including R+M plus O**, ensuring the same profile first admits parsing, indexing and planning. Assert exact O after old ownership ends. Corrupt emitted touched/untouched bytes, a non-name byte, entry/key/file set and a replacement belonging to the wrong qualified target; streaming authentication must reject each. These are emitter/ownership tests, not substitute proof of actual output semantic admission. Parent should retain the later whole-Project equality requirement explicitly.
|
||||
|
||||
## Quoted-text bound and future hooks
|
||||
|
||||
The proposal's `decoded UTF-8 bytes D <= raw payload UTF-8 bytes P` is sound for valid JSON strings, including escapes and surrogate pairs. P must exclude exactly the two ASCII quotes. Reserving P before decode and releasing P-D is a straightforward conservative logical-text bound. It can reject escape-heavy source whose decoded retained text would fit; that rejection is a real selected policy change for the opt-in path, not exact minimal acceptance. An escaped/unescaped pair with identical decoded text should document this tradeoff. No second decoder or Unicode normalization claim is warranted.
|
||||
|
||||
The scanner already decodes once for validation, before the grammar decoder. Therefore grammar-only hooks cannot claim preadmission of that earlier temporary. If it enters the selected domain, its reservation must occur inside the scanner after token extent discovery and before validation, then release before the later grammar decode. A post-next callback is too late. Adding this hook changes a shared API/error-transport seam; it should have independent invalid-string and legacy precedence controls, not be hidden in a retained-text patch.
|
||||
|
||||
If parent instead chooses a parser producer milestone next, narrow it first to **retained syntax text T and its once-only transfer**. List every counted String producer and temporary UUID/declaration-key disposition, keep U/M phase checks explicitly unchanged, and do not claim complete before-growth execution acceptance. The most consequential tests are copied ordinary reference/key overlap, escaped strings at P versus D caps, temporary UUID release and no double charge in resolver. This is still a meaningful prerequisite, but should not silently grow into traversal and all scratch in the same change.
|
||||
|
||||
## Compatibility, maintainability and stack
|
||||
|
||||
The accepted carried route is opt-in even when C=0; legacy constructors must keep their original admission and diagnostic timing. Any new hooks need an explicit mode boundary, not numeric-zero detection. Avoid cloning Budget, resetting live Usage or rebuilding policy from defaults. Resolver reconciliation must verify/transfer admitted retained counts, not reserve them twice.
|
||||
|
||||
A ledger moving from parser to resolver introduces subtleties: U is monotone grammar work, value-end/token-end metadata contains deliberate conservative retention, and scratch is genuinely released. Treating all three as interchangeable live Vec sizes risks weaker caps or double counting. Keep a short authoritative ownership table and tests for those distinct rules; a universal allocation abstraction is unnecessary.
|
||||
|
||||
The recent stack regression demonstrates that even an inline policy field can enlarge many ancestor frames. It does not prove producer hooks are unsafe, nor that more hooks solve stack usage. Keep policy in existing Budget/syntax, retain the slim owner guard, use scoped borrowed accounting state, and avoid large inline callback/guard enums on recursive paths. Preserve the nine existing stack cases and the earlier abort evidence. Neither emission nor producer accounting earns a universal stack guarantee.
|
||||
|
||||
Unresolved before accepted execution: the exact selected pre-growth retained/scratch domain; actual-output parser/resolver temporal coverage; E-reserved strict whole-intent equality; owning accepted-result construction and install/history lifecycle. The bounded emission step deliberately leaves these visible rather than letting a larger parser refactor substitute for actual rename output.
|
||||
@@ -0,0 +1,70 @@
|
||||
# Next parser producer hooks — bounded proposal
|
||||
|
||||
2026-09-25. **Proposal, not accepted requirements or implemented behavior.** Read-only source pin `fa7a980418ef99d772e67c66c28dfa7b5181f875`. Paths below start at `crates/oa-core/src/`. No source edits, builds, runtime execution or Gitea activity. Scope follows the accepted carried-phase contract and domain audit: logical auxiliary entries/occurrences/text, not universal heap/RSS/stack accounting.
|
||||
|
||||
## Recommendation
|
||||
|
||||
The already-counted retained syntax representations can be admitted before their growth with a small set of named producer hooks and one carried ledger. Preserve current work Usage separately, transfer existing reservations rather than count them again at resolve, and use exact lengths for copies. Quoted-string payload length is a safe conservative reservation before the existing decoder, with exact release afterward.
|
||||
|
||||
This closes **named retained-representation timing gaps**, not every current parser allocation. Traversal, scanner-validation Strings and certain parser scratch tables require explicit scope decisions below. The accepted broad before-growth obligation cannot be called completely discharged while those questions are unresolved. No universal allocator interception, second numeric parse, new decoder, or large whole-source multiplier is necessary.
|
||||
|
||||
## Existing domain and minimum producer inventory
|
||||
|
||||
C is the surviving owned-bundle baseline; U remains cumulative grammar-entry work; M is existing logical metadata. New live text reservations T and named scratch W must participate in aggregate checks with C. Do not globally reduce Limits to account for C: source-byte/token/reference/Exact work is a separate domain.
|
||||
|
||||
| Counted representation | Current producer / minimum pre-growth hook | Transfer, release and reconciliation |
|
||||
|---|---|---|
|
||||
| Syntax entry slots U: project/declaration/include/description, field/value/list items, typed declaration/parameter/expression events | Existing `Parser::syntax_entry` (`language.rs:1168`), `State::entry` (`mixed.rs:98`), typed `Budget::entry` calls in declarations/syntax. Preserve exact charge events; additionally check carried live entries plus prospective U/M/named scratch before the event permits its representation to grow. | Do not increment U twice or reinterpret every Vec element as another grammar event. U's established monotone/conservative syntax baseline transfers once into resolver storage. |
|
||||
| Borrowed file records and per-file bookkeeping | `mixed.rs:296–306` initial file count; parsed-file insertion and final collection `:384–410`. Existing F is reserved before traversal. | Preserve F once; moving the map's file values into canonical Vec does not create another retained F. If implementation temporarily retains two actual coordination containers, name/reserve that overlap rather than hiding it. |
|
||||
| Ordinary value-end entries | `Parser::value`, `language.rs:1298–1307`, immediately before `value_ends.insert`. | Check one new logical entry before insertion (existing-key replacement must not invent a new slot). Existing final metadata may conservatively retain dropped value-end subtotal; do not both carry that subtotal and re-add table length. |
|
||||
| Lexical-owner entries | `language.rs:1627–1630`, before push. | Transfer scalar count to M once. Keep correspondence with declaration occurrences, not unique IDs. |
|
||||
| Token-end metadata (one token-count component) | `edit_positions_from_tokens`, `language.rs:1022`, before collecting `ends` of length significant-token count; currently `mixed.rs:339–355` admits this component at the phase boundary. | Preserve the current **single** tokens.len() metadata component. It is conservative after projection scratch drops. Do not add another tokens.len() merely to label retained Token storage; see distinction below. |
|
||||
| Ordinary EditDeclaration/header/field/value/list slots | Existing `ordinary_position_slots` (`mixed.rs:415+`) and `edit_positions_from_tokens` (`language.rs:1014–1140`). Reserve the existing exact/conservative projected slot total before construction; keep the current seven-per-declaration/header envelope and field/value table semantics unless independently revised. | Positions transfer to retained syntax. Header role keys are borrowed static strings (`language.rs:1114–1127`), **not copied text**. Reserve counting-walk work before its own push, or derive the projection count incrementally from successful grammar events without another traversal. |
|
||||
| Typed retained names, references, display names/descriptions/symbols | `Cursor::name` exact borrowed spelling before `to_owned` (`cursor.rs:67–79`); `Cursor::quoted` before existing decode (`:81–95`). | Name/reference strings move into typed declarations once. Reserve decoded-string upper bound, reconcile to exact length, and transfer surviving text. UUID spelling temporaries consumed by identity parsing must release when dropped; they do not appear in the retained syntax text oracle. |
|
||||
| Ordinary retained aliases/names/includes/descriptions/reference/word strings | `Parser::identifier/quoted` (`language.rs:1194–1210`), Ast word copy (`:1370+`), project/declaration/include consumers. | Exact spelling length before copy; quoted rule below. Distinguish retained use from consumed UUID spelling and temporary declaration-kind word. The latter must not become retained T accidentally. On an aborted parse the entire phase unwinds; no reusable ledger/partial result escapes. |
|
||||
| Ordinary field keys and their distinct copies | `Parser::field` (`language.rs:1250+`); declaration fields/key-span map insertion (`:1515–1529`); nested `fields` (`:1230–1246`). | Initial key ownership transfers to one retained map; reserve additional key.len() before each clone. Top-level declaration field_spans owns a second key. Nested-field `key.clone()` coexists with the original local key until scope end: either account/release that temporary or move the key after a duplicate check, preserving error order. |
|
||||
| Projected copied field keys/reference spellings | `Positions::fields/value`, `language.rs:1047–1090`. | Reserve exact existing key/name lengths before cloning, aggregate with still-live AST copies. They remain in T; Atom projection does **not** copy decoded literal text. Existing T45 ordinary-description test is a useful discriminating oracle. |
|
||||
| Resolve syntax inventory / counting work | `mixed/project/accounting.rs:233–352`. | Consume parser's checked U/M/T reservations once; don't seed C then add already-carried syntax totals again. Keep the existing bounded walk for independent reconciliation/LowerStorage calculation if useful, but change its text action from re-reservation to verification. Any walk work still reserves/release entries before growth. Require equality with the established final inventory (including conservative metadata), not silent subtraction after over-allocation. |
|
||||
|
||||
### Tokens versus metadata
|
||||
|
||||
`lex::tokens` charges source bytes once and every **raw** scanner token, including trivia, before scanner advance (`lex.rs:19–45`). It stores only significant tokens (`:47–52`). Current mixed module documentation says tokens themselves use the token bound, while its one tokens.len() entry component represents token-end metadata. That is the declared existing metric even though both token Vec and projection ends can coexist physically.
|
||||
|
||||
Do not silently add retained significant tokens as a second auxiliary entry family, nor claim the old single metadata component separately counts both physical Vecs. The minimal proposal preserves raw-token prechecks and token-bound-only retained storage, and reserves the one established token-end component before its producer. Any change to count significant-token Vec entries separately is an explicit metric revision with new oracles, not a timing repair.
|
||||
|
||||
## Quoted text: raw payload bound is valid
|
||||
|
||||
For a syntactically valid JSON string token, let P be the **UTF-8 byte length between its surrounding ASCII quotes** (checked token.len()-2), and D the decoded Rust String.len(). Then **D <= P**:
|
||||
|
||||
- Unescaped valid UTF-8 scalar text copies its bytes unchanged.
|
||||
- Each simple escape (`\"`, `\\`, `\/`, `\b`, `\f`, `\n`, `\r`, `\t`) occupies two ASCII source bytes and decodes to one byte.
|
||||
- One nonsurrogate `\uXXXX` occupies six ASCII bytes and decodes to at most three UTF-8 bytes.
|
||||
- A valid surrogate pair occupies twelve ASCII bytes and decodes to one scalar of four UTF-8 bytes.
|
||||
- Empty payload has P=D=0. JSON decoding does not Unicode-normalize text or expand entities; invalid escapes/unpaired surrogates are rejected, not larger valid strings.
|
||||
|
||||
Therefore reserve P owned-text bytes against the live ledger **before the existing `serde_json::from_str` call**, then on success assert/check D<=P and release P-D. Move the D reservation with the returned String, or release D when a temporary UUID spelling is consumed and dropped. Use byte lengths, not character counts. Overflow/subtraction failures refuse safely; do not use saturating arithmetic to accept malformed bounds. The envelope may transiently reject an escaped string whose final decoded bytes would fit; that is an explicit conservative pre-growth tradeoff, not a claim of exact minimal admission. No second decoder, escaped-length pass or Exact conversion is required.
|
||||
|
||||
This bound covers logical String bytes only. It does not count String capacity, serde scratch allocation or allocator overhead. A decode failure may have transient partial internal allocation bounded by the raw string extent, but that is not an exact internal-heap claim; the attempt fails and returns no witness.
|
||||
|
||||
### Important earlier existing decode
|
||||
|
||||
`raw_tokens::Scanner::next` scans a quoted token and already calls `serde_json::from_str::<String>` to validate it (`raw_tokens.rs:107–130`, allocation at126). The grammar later decodes it again. Merely hooking Cursor::quoted/Parser::quoted therefore cannot claim **every** decoded String was pre-reserved: scanner validation has already created and dropped one.
|
||||
|
||||
Label that validation-only decoded String as a distinct parser temporary; it is not retained T. It is not automatically covered by the accepted ordinary model/serde-conversion exclusion merely because serde_json produces it. Those exclusions concern model conversion/raw validation domains, not a generic exemption for every parser String. If parent requires this named parser temporary within the before-growth envelope, add an optional private reservation/release hook **inside the scanner immediately before its existing validation call**, after its span is known. Reserve P, drop validation value, release P; later grammar decode receives its separate nonoverlapping reservation. A callback after `scanner.next()` is too late. Do not add a third validation/decoding pass. Public scanning behavior and raw-token work semantics must remain unchanged.
|
||||
|
||||
## Named scratch and traversal decisions — not an invitation to all-heap accounting
|
||||
|
||||
The current inventory does not separately Ledger-meter every structure below. They cannot be declared fully covered merely because another bound exists. Choose their status explicitly before final verification acceptance:
|
||||
|
||||
- Projection counting Vec (`mixed.rs:418+`) and delimiter stack (`language.rs:1023–1030`): they are narrow borrowed work frontiers. A reserve-before-push/release-after-pop hook fits the existing auxiliary-work model, but adds explicitly named scratch peaks; it must not double count token-end metadata. Prefer borrowing/taking existing parsed value ends where possible without a second parse, only if semantic coverage remains identical.
|
||||
- Typed duplicate-name sets clone parameter/override names (`declarations.rs:293,610`). These are temporary syntax coordination copies absent from retained T. Reserve their live map entries and copied name bytes before insertion, release when the set drops, or use borrowed names where lifetimes permit. Do not count them as new retained names. Whether these already-existing temporary sets join this patch's declared domain needs parent approval.
|
||||
- Ordinary open-declaration stack and local String clones: distinguish the existing grammar U reservation from additional simultaneously live coordination. Avoid adding hidden copied strings; any newly introduced work structures require explicit reservations.
|
||||
- Shared traversal owns folded-path table, visited/active/children/stack and normalized/copied path/location strings (`source_project/traversal.rs:9–16,59–87,128,144–180`). Current file/include/depth prechecks bound workload, not every overlapping map slot/owned path byte. If these are included now, use a localized optional private accounting hook at actual insert/push/copy/drop points, retaining public behavior through a no-op path. `path.to_lowercase()` can expand UTF-8 spelling; original path.len() is **not** automatically its bound. Prefer a checked chars-based length sum followed by the existing conversion, or avoid its owned copy with a proved equivalent borrowed comparator; no general huge multiplier. Do not assume all visitation structures share one F slot.
|
||||
|
||||
Raw model construction/serde conversion and architectural validation heaps retain the accepted exclusions; retained mapped warnings are already charged separately. BigInt limbs remain under numeric policy, not this text ledger. Native stack size and allocator/RSS are outside the logical metric. None of these exclusions permits newly owned output strings/map/path keys or actual retained syntax copies to disappear.
|
||||
|
||||
## Bounded next decision
|
||||
|
||||
Yes: pre-growth admission for the **named existing retained auxiliary inventory** is achievable with producer hooks, decoded-text reconciliation and once-only transfer. No: that statement alone does not prove every declared parser-temporary obligation has been met. Parent must pin the scratch/scanner/traversal rows it intends to include or leave explicitly unresolved; doing so is a finite representation-table decision, not universal allocation metering.
|
||||
|
||||
Recommended next design is a producer ledger for U/M/T with per-copy hooks and raw-payload quoted reservations, plus explicitly selected borrowed-work scratch. Keep policy exact and legacy behavior isolated. Prove constructor/ownership transfer and independent cap oracles before integrating actual output verification. Stop here: no requirement is amended, no stronger guarantee accepted, and no code is proposed as already tested.
|
||||
@@ -0,0 +1,116 @@
|
||||
# Exhaustive authored mixed typed-index oracle
|
||||
|
||||
2026-09-24. Independent source/data derivation for private source `aaec78fcc9b273d1485497126392afdd1fb2e54e`. Reuse fixture and independent model construction in `crates/oa-core/src/language/analysis_source/mixed/project/tests.rs:254–330`, `expected` at line 52, and the explicit site oracle at lines 331–538. No Index, lowerer, formatter, parser or compiler executed to produce expectations. Numeric spans below were checked using direct unique substring locations in independently reconstructed literal source; all original fixture bytes are ASCII/LF. This is suggested test data, not a passing-test report.
|
||||
|
||||
## Fixture identity and file binding
|
||||
|
||||
Use the existing `id(n)` helper (line 9): it formats n as twelve decimal digits inside a UUID then parses that UUID. **Do not substitute `Id::from_u128(n)`**: for n=10 the fixture suffix is `000000000010`, not `00000000000a`.
|
||||
|
||||
Retain entry `main.arcadia`; canonical numeric files are 0=`a.arcadia` (213 bytes), 1=`b.arcadia` (213 bytes), 2=`main.arcadia` (474 bytes), aggregate 900 original bytes. Include spelling/order in main is b then a; numeric file order is canonical a then b. File 0 has calculation f/id(10), file 1 g/id(11); both declare parameter x/id(1), y/id(12). Parameter x UUID also equals the Project's global id(1).
|
||||
|
||||
Main's ordinary node n points forward to typed property v; property v's subject points forward to ordinary activity w. Its base is exactly `@f(y: 2, x: @g(x: 3, y: 4)) + @f(x: 5, y: 6)`. Typed declarations begin after the Project header, includes and ordinary node. Reuse the existing independently built full Project, including the diagram/node defaults and both calculation bodies; do not replace full equality with an index-only projection.
|
||||
|
||||
Notation: `G(n)` = `Target::Global(id(n))`; `Q(c,p)` = `Target::Parameter { calculation: id(c), parameter: ParameterId(id(p)) }`. Paths are complete **root-to-leaf** `Child` sequences: L=Left, R=Right, A1=Argument(ParameterId(id(1))), A12=Argument(ParameterId(id(12))). `[]` is a real empty expression-root path. `—` means `None`, not `Some(empty)`. All spans are half-open full-original-file byte spans.
|
||||
|
||||
## Entire expected names catalog (G=11)
|
||||
|
||||
| Qualified target | Name | Original declaration anchor |
|
||||
|---|---|---|
|
||||
| G(1) | p | main: `project p` |
|
||||
| G(2) | w | main: `oa_activity w` |
|
||||
| G(3) | v | main: `oa_property v` |
|
||||
| G(4) | n | main: `node n` |
|
||||
| G(5) | d | main: `diagram d` |
|
||||
| G(10) | f | a: `calculation f` |
|
||||
| G(11) | g | b: `calculation g` |
|
||||
| Q(10,1) | x | a: `input x` |
|
||||
| Q(10,12) | y | a: `input y` |
|
||||
| Q(11,1) | x | b: `input x` |
|
||||
| Q(11,12) | y | b: `input y` |
|
||||
|
||||
Global p/w/n/d names are required even though this typed index must contain **no ordinary declaration/reference occurrences** for them. There are 7 global names plus 4 scoped names; every name is one UTF-8 byte.
|
||||
|
||||
## Entire expected expression map (E=15)
|
||||
|
||||
Each row is `(ExpressionKey { owner, field, path }, Location { file, span })`. E labels are explanatory, not persisted arena positions or part of key identity. The order below matches the authored lower-site oracle only for ease of comparison; compare a complete map/set of keys rather than relying on BTree ordering of Path.
|
||||
|
||||
| Row | File | Owner | Field | Path | Span | Exact source slice / unique anchor |
|
||||
|---|---:|---:|---|---|---|---|
|
||||
| E0 | 0 | 10 | Body | [] | 202..209 | `$x + $y` |
|
||||
| E1 | 0 | 10 | Body | [L] | 202..204 | `$x` |
|
||||
| E2 | 0 | 10 | Body | [R] | 207..209 | `$y` |
|
||||
| E3 | 1 | 11 | Body | [] | 202..209 | `$x - $y` |
|
||||
| E4 | 1 | 11 | Body | [L] | 202..204 | `$x` |
|
||||
| E5 | 1 | 11 | Body | [R] | 207..209 | `$y` |
|
||||
| E6 | 2 | 3 | Base | [] | 290..334 | `@f(y: 2, x: @g(x: 3, y: 4)) + @f(x: 5, y: 6)` |
|
||||
| E7 | 2 | 3 | Base | [L] | 290..317 | `@f(y: 2, x: @g(x: 3, y: 4))` |
|
||||
| E8 | 2 | 3 | Base | [L,A12] | 296..297 | `y: 2`, offset 3, length 1 |
|
||||
| E9 | 2 | 3 | Base | [L,A1] | 302..316 | `@g(x: 3, y: 4)` |
|
||||
| E10 | 2 | 3 | Base | [L,A1,A1] | 308..309 | `x: 3`, offset 3, length 1 |
|
||||
| E11 | 2 | 3 | Base | [L,A1,A12] | 314..315 | `y: 4`, offset 3, length 1 |
|
||||
| E12 | 2 | 3 | Base | [R] | 320..334 | `@f(x: 5, y: 6)` |
|
||||
| E13 | 2 | 3 | Base | [R,A1] | 326..327 | `x: 5`, offset 3, length 1 |
|
||||
| E14 | 2 | 3 | Base | [R,A12] | 332..333 | `y: 6`, offset 3, length 1 |
|
||||
|
||||
There are 3 roots and 12 nonroot sites, hence **P=12 distinct allocated shared Path Steps** under one-Step-per-child construction. Maximum expression depth including root is 4. Same spans 202..209 and parameter token positions in a/b are deliberately different owners/files.
|
||||
|
||||
## Entire expected occurrence map (H+O=21)
|
||||
|
||||
Each row spells every `OccurrenceKey` field: owner, optional field, optional path, token role and qualified target, plus physical file/span. `Decl` means `TokenRole::Declaration`; every other role below means `TokenRole::Use(lower::Role::<name>)`. Input1/Input12 mean `Field::Input(ParameterId(id(1/12)))`. Every token length is 1, with @/$ excluded.
|
||||
|
||||
| Row | File | Owner | Field | Path | Role | Target | Span | Exact spelling / independent source anchor |
|
||||
|---|---:|---:|---|---|---|---|---|---|
|
||||
| H0 | 0 | 10 | — | — | Decl | G(10) | 12..13 | f; `calculation f`, +12 |
|
||||
| H1 | 1 | 11 | — | — | Decl | G(11) | 12..13 | g; `calculation g`, +12 |
|
||||
| H2 | 2 | 3 | — | — | Decl | G(3) | 206..207 | v; `oa_property v`, +12 |
|
||||
| O0 | 0 | 10 | Input1 | — | ParameterDeclaration | Q(10,1) | 68..69 | x; `input x`, +6 |
|
||||
| O1 | 0 | 10 | Input12 | — | ParameterDeclaration | Q(10,12) | 128..129 | y; `input y`, +6 |
|
||||
| O2 | 0 | 10 | Body | [L] | ParameterUse | Q(10,1) | 203..204 | x; `$x`, +1 |
|
||||
| O3 | 0 | 10 | Body | [R] | ParameterUse | Q(10,12) | 208..209 | y; `$y`, +1 |
|
||||
| O4 | 1 | 11 | Input1 | — | ParameterDeclaration | Q(11,1) | 68..69 | x; `input x`, +6 |
|
||||
| O5 | 1 | 11 | Input12 | — | ParameterDeclaration | Q(11,12) | 128..129 | y; `input y`, +6 |
|
||||
| O6 | 1 | 11 | Body | [L] | ParameterUse | Q(11,1) | 203..204 | x; `$x`, +1 |
|
||||
| O7 | 1 | 11 | Body | [R] | ParameterUse | Q(11,12) | 208..209 | y; `$y`, +1 |
|
||||
| O8 | 2 | 3 | Subject | — | Reference | G(2) | 269..270 | w; `subject @w`, +9 |
|
||||
| O9 | 2 | 3 | Base | [L] | CallTarget | G(10) | 291..292 | f; `@f(y:`, +1 |
|
||||
| O10 | 2 | 3 | Base | [L] | ArgumentLabel | Q(10,12) | 293..294 | y; `y: 2`, +0 |
|
||||
| O11 | 2 | 3 | Base | [L] | ArgumentLabel | Q(10,1) | 299..300 | x; `x: @g`, +0 |
|
||||
| O12 | 2 | 3 | Base | [L,A1] | CallTarget | G(11) | 303..304 | g; `@g(x:`, +1 |
|
||||
| O13 | 2 | 3 | Base | [L,A1] | ArgumentLabel | Q(11,1) | 305..306 | x; `x: 3`, +0 |
|
||||
| O14 | 2 | 3 | Base | [L,A1] | ArgumentLabel | Q(11,12) | 311..312 | y; `y: 4`, +0 |
|
||||
| O15 | 2 | 3 | Base | [R] | CallTarget | G(10) | 321..322 | f; `@f(x: 5`, +1 |
|
||||
| O16 | 2 | 3 | Base | [R] | ArgumentLabel | Q(10,1) | 323..324 | x; `x: 5`, +0 |
|
||||
| O17 | 2 | 3 | Base | [R] | ArgumentLabel | Q(10,12) | 329..330 | y; `y: 6`, +0 |
|
||||
|
||||
Totals: H=3 typed global headers. O=18 lower occurrences: 4 parameter declarations, 4 parameter uses, 1 subject reference, 3 call targets, 6 labels. Header occurrences are produced by indexing and are absent from the retained lower O vector. Repeated f labels have different full paths; nested g labels differ by both path and calculation scope. Ordinary node's `element @v` remains absent from this typed occurrence map.
|
||||
|
||||
For executable expected records, reuse the existing unique-anchor `span(source, context, offset, len)` helper at line 156; assert the numeric values above for the unmodified fixture. Build expected paths from these literal Child arrays, not by following `resolved.expressions.parent` or enumerating actual Index keys. Construct expected keys/targets explicitly from this table. Exhaustive equality must compare all 11 names, all 15 expressions, all 21 occurrences, and the entire independent Project.
|
||||
|
||||
## Exact added retained storage and scratch anchors
|
||||
|
||||
Under accepted contract dimensions and current three-map/shared-Step design:
|
||||
|
||||
| New retained representation | Entry slots | Occurrence slots | Owned text bytes |
|
||||
|---|---:|---:|---:|
|
||||
| Qualified names | 11 | 0 | 11 |
|
||||
| Occurrence map | 21 | 0 | 0 |
|
||||
| Expression map | 15 | 0 | 0 |
|
||||
| Unique Path Steps | 12 | 0 | 0 |
|
||||
| **Added retained total** | **59** | **0** | **11** |
|
||||
|
||||
Thus final Ledger is exactly the incoming accepted-witness Ledger plus `(entries:59, occurrences:0, text:11)` if no additional retained representation is introduced. The baseline retains the raw 15 expression-site entry slots and 18 occurrence slots; neither is released merely because indexed versions now exist. Original source bytes 900 are borrowed and are not added again to text storage.
|
||||
|
||||
Known scratch sizes are owner map 3, physical-token set 21, path lookup Vec 15, call map 3, reverse-name maps 11, syntax-site lookup 15. The original all-roots-first LIFO syntax traversal's peak work Vec has 5 elements (root main pops, right call contributes x/y while a/b roots and left call remain). This 5 is specific to the present traversal order, not a requirement to preserve that order. A final retained delta is fixed; a whole-construction cap must separately account for the chosen scratch lifetime/ordering and for any earlier resolution peak. Do not claim `baseline.entries+59` is a sufficient construction cap while scratch is alive. With all original scratch still alive at syntax-walk peak, scratch is 3+21+15+3+11+15+5=73; explicit earlier releases can reduce this overlap. Any implementation reserving conservative bounds should state the difference instead of weakening exact semantic oracles.
|
||||
|
||||
## Corruption that must reach the call-site check
|
||||
|
||||
Before Index construction, mutate only O11 (main 299..300, owner3, Base, expression site7 / path[L]) from target `Q(10,1)` to `Q(11,1)`. Keep source, tokens, owner, field, expression and span unchanged.
|
||||
|
||||
This is deliberately not an unknown-target or spelling error: both calculations have x/id(1); both qualified target names are `x`. The token is the same full Word inside its expression/owner. The changed key does not collide with nested g's label because that label has path[L,A1], and physical ownership remains unique. ArgumentLabel role accepts a scoped target with a path. The call at path[L] still targets G(10), so `index.rs:438–448` must reject with `Argument label crosses its called calculation.` at span299..300. Assert this diagnostic, not merely any error. Repeating with O10's y Q(10,12)->Q(11,12) is equivalent but unnecessary duplication.
|
||||
|
||||
A distinct runtime omission negative removes O11 while leaving its argument child E9: the qualified-label check (`index.rs:455–469`) must reject `Argument child has no qualified label.` at E9 span302..316. By contrast removing O0 or O8 is an exhaustive-output-oracle failure, not a promised current runtime rejection.
|
||||
|
||||
## Optional direct extensions without changing oracle provenance
|
||||
|
||||
- **SA:** apply the same rich additions to `bundle(Layer::Sa)` and construct model from `expected(Layer::Sa)`. Its system s/id(6) adds one global name. H/O/E/P and all expression/token spans through main's base remain the same because `sa_property` and `oa_property` have equal length and the system/diagram-layer differences occur later. G=12, new retained entries=60, name bytes=12. Assert the system in the complete Project and catalog; it contributes no typed occurrence.
|
||||
- **Unicode/CRLF:** independently prefix each file with `// 雨\r\n` (8 UTF-8 bytes) and replace each original LF with CRLF. For any original endpoint b in these ASCII files, the transformed byte endpoint is `8 + b + count(original[..b] == LF)`. This raw-byte transformation is independent of parsing/indexing and applies separately to start/end. Or use the same unique literal anchors against transformed sources; Rust string find returns byte offsets. Keep exact original bundle comparison. Do not carry the ASCII numeric offsets unchanged into this variant.
|
||||
@@ -0,0 +1,51 @@
|
||||
"""Parent raw-byte check of independently authored fixture/index expectations."""
|
||||
from pathlib import Path
|
||||
import json
|
||||
import re
|
||||
|
||||
def ident(n):
|
||||
return f'00000000-0000-0000-0000-{n:012}'
|
||||
|
||||
body = (f'node n id "{ident(4)}" {{ diagram @d; element @v; }}\n'
|
||||
f'oa_property v "Value" id "{ident(3)}" {{ subject @w; type Number; base 1; }}\n'
|
||||
f'oa_activity w "Work" id "{ident(2)}" {{}}\n'
|
||||
f'diagram d "Diagram" id "{ident(5)}" {{ }}\n')
|
||||
main = f'arcadia 8;\nproject p "Project" id "{ident(1)}" {{\n{body}\n}}\n'
|
||||
main = main.replace('base 1;', 'base @f(y: 2, x: @g(x: 3, y: 4)) + @f(x: 5, y: 6);')
|
||||
main = main.replace('node n', 'include "b.arcadia"; include "a.arcadia";\nnode n')
|
||||
files = []
|
||||
for alias, name, n, op in [('f', 'F', 10, '+'), ('g', 'G', 11, '-')]:
|
||||
files.append((f'calculation {alias} "{name}" id "{ident(n)}" {{\n'
|
||||
f'input x id "{ident(1)}" : Number;\n'
|
||||
f'input y id "{ident(12)}" : Number;\n'
|
||||
f'result Number; body $x {op} $y;\n}}\n').encode())
|
||||
files.append(main.encode())
|
||||
assert list(map(len, files)) == [213, 213, 474]
|
||||
oracle = Path(__file__).with_name('ORACLE.md').read_text(encoding='utf-8')
|
||||
rows = [line.split('|')[1:-1] for line in oracle.splitlines() if re.match(r'\| (E|H|O)\d+ \|', line)]
|
||||
expressions = [r for r in rows if r[0].strip().startswith('E')]
|
||||
occurrences = [r for r in rows if not r[0].strip().startswith('E')]
|
||||
expected_expressions = ['$x + $y', '$x', '$y', '$x - $y', '$x', '$y',
|
||||
'@f(y: 2, x: @g(x: 3, y: 4)) + @f(x: 5, y: 6)',
|
||||
'@f(y: 2, x: @g(x: 3, y: 4))', '2', '@g(x: 3, y: 4)', '3', '4',
|
||||
'@f(x: 5, y: 6)', '5', '6']
|
||||
expected_occurrences = ['f', 'g', 'v', 'x', 'y', 'x', 'y', 'x', 'y', 'x', 'y',
|
||||
'w', 'f', 'y', 'x', 'g', 'x', 'y', 'f', 'x', 'y']
|
||||
assert len(expressions) == 15 and len(occurrences) == 21
|
||||
checks = []
|
||||
for group, expected, span_column in [(expressions, expected_expressions, 5),
|
||||
(occurrences, expected_occurrences, 7)]:
|
||||
for row, value in zip(group, expected, strict=True):
|
||||
name, file = row[0].strip(), int(row[1])
|
||||
start, end = map(int, row[span_column].strip().split('..'))
|
||||
assert files[file][start:end] == value.encode(), (name, files[file][start:end], value)
|
||||
transformed = b'// \xe9\x9b\xa8\r\n' + files[file].replace(b'\n', b'\r\n')
|
||||
shifted_start = 8 + start + files[file][:start].count(b'\n')
|
||||
shifted_end = 8 + end + files[file][:end].count(b'\n')
|
||||
assert transformed[shifted_start:shifted_end] == value.encode()
|
||||
checks.append(name)
|
||||
assert 11 + 21 + 15 + 12 == 59
|
||||
print(json.dumps({'result': 'PASS', 'file_bytes': list(map(len, files)),
|
||||
'original_span_checks': len(checks), 'unicode_crlf_span_checks': len(checks),
|
||||
'retained_entries': 59, 'retained_name_bytes': 11,
|
||||
'scope': 'Independent raw fixture/byte arithmetic, no parser, lowerer, index or compiler execution.'}))
|
||||
@@ -0,0 +1,74 @@
|
||||
# Private mixed typed-index source review
|
||||
|
||||
Parent publication note: production was frozen at
|
||||
`a18b7966e5ac6f5526deecdc4dd6ae8ca0e5ac51` with all three final reviewed production
|
||||
hashes below verified unchanged. Final test bytes at that commit are
|
||||
`f7561e05fb6c03f6d4926bb1583f87c91dabccc4370d221d80b1f9251d994caf`;
|
||||
they include the corrected nested-abs lifecycle fixture after the reviewer's
|
||||
supporting test snapshot. Successor `1d02206c57d5c32caa67f08c6e58f345c0fb5970`
|
||||
changes only an equivalent test range pattern and an implementation note after
|
||||
the first strict lint failure. The original review's source-only scope and
|
||||
findings are retained; this note does not claim runtime acceptance.
|
||||
|
||||
2026-09-24. Reviewer: `/root/mixed_index_source_review` (GPT-6 Astra). Parent owns acceptance, publication, and integration. This is local-only preliminary source evidence; no compiler, test execution, Gitea mutation, source edit, or commit was performed by this reviewer. Issue 37 and its frozen Windows prerelease source were not touched.
|
||||
|
||||
## Scope and exact source
|
||||
|
||||
Reviewed the uncommitted production delta in `.local/as3batch`, branch `codex/analysis-mixed-index-956atf-20260924`, HEAD/base `aaec78fcc9b273d1485497126392afdd1fb2e54e`, against root `harness/analysis-mixed-index-contract-20260924.md` and its accepted independent design review. Paths below are relative to `crates/oa-core/src/` in that checkout. Parent-owned AGENTS.md changes are excluded.
|
||||
|
||||
Initial SHA-256 source inventory (after first source read, before reported repairs):
|
||||
|
||||
| File | SHA-256 |
|
||||
| --- | --- |
|
||||
| `language/analysis_source/index.rs` | `BEC770704DE957134AD05DC6FFA1117F114B5AC8787032A312B7D31B28FA05F5` |
|
||||
| `language/analysis_source/mixed/project.rs` | `EDC2D182FFC2F03F4039872A8B392174FF78185DD41F0A1B5611C6A93502878C` |
|
||||
| `language/analysis_source/mixed/project/accounting.rs` | `3EE9FF1ECDDAE29B35062DFF4738322C7941DED773D128C20D37EAE2BAAF30F6` |
|
||||
|
||||
Final production source reinspection after builder reported its freeze (SHA-256):
|
||||
|
||||
| File | SHA-256 |
|
||||
| --- | --- |
|
||||
| `language/analysis_source/index.rs` | `7C02DDA3622957D837B4AC39D477AE7842570BF3F0709ED48CD5495652636781` |
|
||||
| `language/analysis_source/mixed/project.rs` | `7FE6EF88729718C38D2FFFD46331CDD03624E1B52108DE0B7609DA4FBBDBAC2A` |
|
||||
| `language/analysis_source/mixed/project/accounting.rs` | `3EE9FF1ECDDAE29B35062DFF4738322C7941DED773D128C20D37EAE2BAAF30F6` |
|
||||
|
||||
New test-source snapshot at that checkpoint: `language/analysis_source/mixed/project/tests/index_tests.rs` SHA-256 `6AEC4BE3CCA5D2F69A47DF3F88CC56BB012142E828305A8E121B7FE0A780A074`. The reviewer inspected corruption/transport/storage tests as supporting source, but did not perform a complete independent test-oracle audit or execute them. `git diff --check` returned exit 0.
|
||||
|
||||
## Concrete findings and current disposition
|
||||
|
||||
1. **Late call checks lost current file ownership.** `IndexData::build` initially updated `Original.at` while populating calls, but not while checking argument-label/callee agreement or argument-child/label agreement. A failure in either later loop could combine the current span with the last file in the preceding occurrence loop. Sent directly to parent and builder. Builder added `sources.at.set(Some(at.file))` and `sources.at.set(Some(site.file))` at the respective loop heads. Source reinspection confirms the repair. Runtime corruption controls must assert the relevant canonical file as well as the message.
|
||||
|
||||
2. **Invalid numeric files acquired fabricated entry-file offsets.** `ResolvedMixedProject::index` initially filtered out-of-range `failure.file` to `None`, then passed the untouched span to `Failure::typed`, whose existing fallback selects the entry file. Corrupting an expression/occurrence file to `files.len()` therefore assigns an unrelated offset to the entry. Parent independently identified the same concern. Builder now explicitly resets the span to `Span::default()` for missing/out-of-range file authority before transport; source reinspection confirms that fallback repair. Valid canonical file failures continue to retain their span.
|
||||
|
||||
3. **UTF-8 split offsets can panic during diagnostic conversion.** An internal expression/occurrence span whose start splits a multibyte character is rejected by the verifier, but `Failure::typed` passes its start to `language::diagnostic`, which slices `&source[..start]` without checking UTF-8 boundaries (`language.rs:178` onward). Thus the required damaged-span rejection could panic while reporting the intended error. Sent directly to parent and builder. Builder now tests the full error span with `source.get(start..end)` at the mixed consumed-constructor transport seam. Invalid UTF-8/range spans reset to `Span::default()` while retaining a known canonical file; unknown/out-of-range file authority uses the entry default. Source reinspection confirms the repair, and the added source test specifically chooses a byte inside `雨` and asserts the real `a.arcadia` path with default span. It does not alter unrelated public diagnostic code. Runtime execution remains pending. This was a concrete source control-flow issue reachable through required internal corruption, not through arbitrary fabrication of the sealed witness.
|
||||
|
||||
## Ownership and compatibility assessment
|
||||
|
||||
- `ResolvedMixedProject::index(self)` consumes the retained witness and moves it unchanged into an opaque `IndexedMixedProject`. It neither clones the complete Project nor reparses/re-lowers any source. Its views borrow the wrapper, whose retained syntax still borrows the original `SourceBundle`.
|
||||
- `IndexInput`'s private `files` field seals construction within the witness module; no method exports an input value. Shared verifier access to project/sites and borrowed original tokens does not expose an authenticated sibling constructor accepting replaceable source/token maps. No self-reference is stored in the output. The nested input lifetime/covariance still requires compilation to establish Rust acceptance; source design does not require unsafe code.
|
||||
- `IndexedMixedProject` has no `Deref`, `AsRef`, conversion, or updater adapter to legacy `Index`. `update::Plan`/`Composition` still use the nominal legacy type. Arbitrarily constructible internal `IndexData` alone cannot construct either provenance wrapper.
|
||||
- The mixed catalog streams Project, definitions, elements, relations, diagrams, diagram nodes/placements, artifact links, and traces exactly once, plus `(calculation, parameter)` names. This matches all `Project::identities` categories without allocating its temporary Vec. Scoped parameter UUID reuse cannot collide with globals or parameters of another calculation.
|
||||
- Typed-only name construction, borrowed numeric sources, output wrapper, and local budget checks are preserved. Shared `Storage(None)` adds no reservations; the existing local occurrence-cap and scalar-visited checks remain enabled only on that path. Owner/expression/token/call/structural check order remains equivalent, aside from earlier scratch destruction and unobserved diagnostic attribution. No typed-only semantic regression was identified by source comparison; test/compiler compatibility is not claimed here.
|
||||
|
||||
## Structural and token invariants
|
||||
|
||||
The extracted verifier retains owner containment; binding-field validity; parent-before-child rejection of forward/cyclic edges; file/owner/field agreement; proper subtree spans; unique qualified expression paths; role/qualified-target checks; whole original Word-token boundaries and spelling; duplicate physical-token/qualified occurrence rejection; call-label/callee agreement; required label existence for argument-child edges; and authored-syntax traversal comparing actual child edges to arena claims. The syntax-site lookup and visited total reject missing/extra expressions. It still intentionally trusts sealed lowering for complete non-expression reference/parameter occurrence emission; no additional completeness claim follows from the runtime checks.
|
||||
|
||||
## Aggregate live-storage review
|
||||
|
||||
Let G be global-plus-scoped names, H typed headers, O raw occurrences, E raw expression sites, P nonroot sites, C calls, W live syntax work items. The source adds retained entry slots `R = G + H + O + E + P`, zero occurrence slots, and exactly copied-name UTF-8 bytes. Prior raw E entries and O occurrence slots remain charged in the consumed ledger throughout.
|
||||
|
||||
- Every owned name reserves one entry and its bytes before copying/insertion.
|
||||
- Each owner reserves one scratch entry before insertion; each new occurrence reserves two entries before its physical-token-set and retained-map insertions.
|
||||
- Each nonroot path reserves one retained Step before `Arc::new`. Each expression reserves two entries before retained-map insertion and scratch-path-vector push. Arc clones allocate no additional Step; enclosing map/vector slots remain charged.
|
||||
- After occurrence validation, owners and physical tokens are explicitly dropped before releasing H and H+O scratch entries. Paths and call entries are explicitly dropped/released after argument-child validation. P shared path-node reservations remain retained while map keys still own those nodes.
|
||||
- Reverse borrowed-name maps reserve G before insertion, syntax-site lookup reserves E incrementally before insertion, and each root/child walk push reserves one entry. A popped work slot is released immediately according to the accepted logical-live-slot policy; the scalar visited total adds no mixed allocation charge. Reverse maps/sites/work are dropped before the final scratch release.
|
||||
- No allocated declaration/file adapter table or copied path vector was introduced. `Path::edges` remains used only by tests. Error transport remains outside successful retained storage, consistent with the existing witness policy.
|
||||
|
||||
The success-path peak expressions are: occurrence completion `R + H + (H + O) + E`; call validation `R + E + C`; syntax traversal `R + G + E + W`, in addition to the inherited baseline. These are source-derived formulas, not measured peaks. Earlier prefixes cannot exceed the first expression under valid monotonic accumulation; alternative fixture peaks still need exact tests. Checked aggregate `Ledger::reserve` precedes all new allocation growth; normal reserve failures return immediately and consume/drop the whole witness, so no stale lease or partial success can escape.
|
||||
|
||||
## Destruction and remaining gates
|
||||
|
||||
Persistent Path comparison/debug/drop remain iterative; final Arc ownership peels parents without recursive Step destruction. New verifier traversal is iterative and retains the inherited frame check. Syntax/Project destruction remains on existing lifecycle policy; runtime success and late-rejection small-stack checks are required, not established by this source review. No parse-byte/token/entry/reference or Exact-work charging calls were added to the mixed index path, and complete admission is not repeated; frame checking may update deepest-frame usage as permitted.
|
||||
|
||||
All three concrete findings are repaired by source inspection at the final production hashes above. No remaining blocking source finding was identified for this bounded implementation. Compilation, runtime and oracle acceptance remain unverified by this reviewer. Required full numeric authored oracles, qualified repeated/reordered calls, all intended corruption controls, exact aggregate cap/cap-minus-one tests, provenance/metadata checks, small-stack execution, inherited typed-only index/update/export compatibility, strict core Clippy, and inherited full-library disposition remain parent acceptance gates. This read-only private index is a prerequisite for complete original-coordinate mixed editing; it does not implement ordinary occurrence indexing, public source8, or the user's full editing/UI parity objective.
|
||||
@@ -0,0 +1,203 @@
|
||||
# Mixed typed occurrence index: successor contract
|
||||
|
||||
Accepted bounded implementation contract, 2026-09-24, after parent source
|
||||
inspection and [independent critique](analysis-mixed-index-review-20260924.md).
|
||||
This is a design decision, not an implemented or verified index. Implementation facts refer to
|
||||
`aaec78fcc9b273d1485497126392afdd1fb2e54e`, not the Windows prerelease.
|
||||
This continues the [complete-Project contract](analysis-mixed-project-contract-20260923.md),
|
||||
especially section 7, and the [original-span composition review](analysis-original-span-review-20260923.md).
|
||||
Issue 7 owns scope/status; issue 37's frozen source is unaffected.
|
||||
|
||||
The requested end state remains complete graphical/textual/API parity for
|
||||
quantities, calculations and constraints, with preservation, checked persistence
|
||||
and usable workflows. A private typed occurrence index is a dependency, not that
|
||||
end state. Ordinary occurrence indexing, update composition, public source8,
|
||||
structural editing, migration, UI and transport acceptance remain required.
|
||||
|
||||
## Current evidence and problem
|
||||
|
||||
- `language/analysis_source/mixed/project.rs:18` owns one admitted complete
|
||||
Project, original `ParsedMixedSyntax`, warnings, static limits/usage, retained
|
||||
expression/occurrence arenas and their aggregate storage ledger. Only Project
|
||||
and warnings are exposed read-only. It does not provide an editing index.
|
||||
- `language/analysis_source/mixed.rs:39` retains the exact borrowed SourceBundle,
|
||||
caller ProjectLimits, canonical file table, full-buffer tokens, original typed
|
||||
declarations and ordinary syntax positions. Numeric file keys belong to this
|
||||
table; equal offsets in different files are unrelated locations.
|
||||
- Existing typed-only `analysis_source/index.rs:122-243` owns names and occurrence/
|
||||
expression maps but borrows a caller numeric source map. Its private builder
|
||||
combines an ordinary Project with separate Lowered model maps. Feeding the
|
||||
complete mixed Project into that path unchanged duplicates typed globals.
|
||||
Its local map-size checks do not account for the retained mixed ledger.
|
||||
- `analysis_source/update.rs` accepts the typed-only Index in its standalone
|
||||
Plan and opaque Composition/OriginalEdits adapter. Those APIs do not establish
|
||||
complete SourceBundle entry/path/profile authority for mixed input.
|
||||
- Public `source_project.rs:93-117` exposes mutable bundle/model/index fields;
|
||||
`update_bundle_with_identity_map` recompiles and compares all three before
|
||||
editing (640 onward). Its ordinary collector applies edits, then runs the
|
||||
structural updater, then recompiles and compares the complete intended Project
|
||||
(868 onward). This later-coordinate structural pass cannot be relabeled as
|
||||
original-coordinate mixed composition. That public contract is unchanged here.
|
||||
|
||||
## Accepted choice
|
||||
|
||||
Criteria are exact original ownership, complete semantic coverage within the
|
||||
declared index scope, aggregate resource accounting, compatibility and avoiding
|
||||
two divergent implementations of structural verification.
|
||||
|
||||
| Candidate | Evidence / tradeoff |
|
||||
| --- | --- |
|
||||
| Shared private verification/maps with distinct provenance wrappers | Reuses the existing structural-path, token, owner and call-label checks. Requires explicit source/declaration/site views and storage reservations. Preferred if the extraction remains small and typed-only behavior is preserved. |
|
||||
| Separate copy of the typed index for mixed input | Simpler initial access to mixed fields, but duplicates the nontrivial syntax-edge and call-label verifier and would need every later correction twice. Reject unless extraction exposes a concrete incompatible invariant. |
|
||||
| Reparse mixed fragments into the old numeric-map Index | Loses complete bundle/profile authority, repeats accounting and can lose original coordinates. Reject. No substring, masked-source or canonicalized bridge. |
|
||||
|
||||
Adopt the first candidate with the consumed mixed wrapper below. The critique's
|
||||
two findings are resolved here: storage domains are fixed by the existing
|
||||
contract, and reference completeness is distinguished from runtime expression
|
||||
verification. Revisit only for a demonstrated incompatible verifier invariant
|
||||
or a concrete requirement for multiple independently retained index owners.
|
||||
|
||||
Do not add a public generic indexing framework. Shared helpers may consume
|
||||
private borrowed inputs only inside the authenticated constructors. There must
|
||||
be no public or sibling-callable API that treats arbitrary Lowered sites and
|
||||
caller-replaced token/location maps as an authenticated mixed index.
|
||||
|
||||
## Ownership and construction invariant
|
||||
|
||||
The proposed mixed constructor consumes `ResolvedMixedProject<'s>` and returns
|
||||
an opaque `IndexedMixedProject<'s>` owning that witness plus its index maps.
|
||||
Names are illustrative API identifiers. The output borrows the original bundle
|
||||
through its retained witness; it contains no self-references. Its read-only
|
||||
views borrow the output. Construction either succeeds completely or returns
|
||||
diagnostics with no usable partial index.
|
||||
|
||||
Consuming the witness prevents creating multiple independently retained indexes
|
||||
while charging only one to its ledger. If a borrowed-construction alternative is
|
||||
chosen, it must prove reservation lifetime/release and simultaneous-index bounds;
|
||||
extra reservation lease machinery is not justified without a demonstrated need.
|
||||
|
||||
The mixed wrapper must not dereference/convert to the old Index or implement an
|
||||
adapter accepted by old `Plan::renames`, `Plan::apply` or `Composition::renames`.
|
||||
Read-only access to names/locations does not grant edit authority. A later
|
||||
compositor must bind the full original entry, file set, canonical paths, bytes,
|
||||
limits/profile and complete target, not merely compare touched file bytes.
|
||||
|
||||
Build from retained full-file tokens/declarations and the existing expression/
|
||||
occurrence arenas without reparsing, lowering again or duplicating Project
|
||||
records. Iterate typed declarations with their canonical numeric file keys;
|
||||
do not clone them into old owned Units. Derive global names exactly once from
|
||||
the complete Project, including project, definitions, elements, relations,
|
||||
diagrams, placements, artifacts and traces. Add calculation parameters under
|
||||
`(calculation_id, parameter_id)`; a parameter UUID reused by another calculation
|
||||
or a global remains distinct. Parameter spelling is calculation scoped.
|
||||
|
||||
Reuse existing expression/occurrence key semantics: file-qualified locations,
|
||||
owner, binding field, structural child path, token role and qualified target.
|
||||
Headers and all typed reference roles must be included. Ordinary targets may
|
||||
appear in the catalog and typed references, but this view does not invent an
|
||||
ordinary declaration/reference occurrence index. Document that distinction in
|
||||
the read-only API and tests.
|
||||
|
||||
Verify owner containment, UTF-8 source boundaries, exact token spelling and
|
||||
token classification, uniqueness of physical token ownership and qualified
|
||||
keys, parent-before-child/path/depth rules, and call-target/parameter-label
|
||||
agreement. Check structural edges against retained authored syntax, not only
|
||||
internally consistent arena claims. Keep file ownership when reporting errors;
|
||||
use an explicit invariant-error location fallback when no one file is justified.
|
||||
|
||||
## Live storage and failure policy
|
||||
|
||||
Continue the retained ledger and Budget/profile; do not initialize a fresh
|
||||
permissive budget or recharge original source/token/exact-number parsing.
|
||||
Reserve checked aggregate additions before allocation, combining the existing
|
||||
syntax/sites/warnings baseline with new retained maps and all live scratch.
|
||||
|
||||
The implementation must inventory names and owned name bytes, occurrence and
|
||||
expression map entries, unique persistent Path nodes, and temporary owners,
|
||||
physical locations, path lookup vector, call map, global/parameter spelling
|
||||
lookups, syntax-site lookup and iterative walk slots. An Arc clone shares a path
|
||||
node but still occupies its enclosing map/vector slot. No copied path vectors
|
||||
are needed to construct keys. If `Project::identities()` is used, account for
|
||||
its temporary Vec (`model.rs:1763`); a streaming traversal avoids that allocation.
|
||||
|
||||
Retained lowerer occurrences and newly retained occurrence-map entries coexist.
|
||||
Preserve the preceding contract's dimensions: lowerer occurrence records retain
|
||||
their occurrence-slot reservations; new index map/vector/set entries and unique
|
||||
Path nodes consume entry slots. Separately owned names consume text bytes.
|
||||
Both representations count once in their respective domains; neither record
|
||||
is charged twice merely because it is called an occurrence. Do not reset or
|
||||
release the old arena's reservation while its vectors remain live. Release scratch reservations only
|
||||
after the corresponding allocations are dropped. Preserve prior logical-slot
|
||||
scope: this is not an allocator-capacity/RSS or raw model/validator-heap bound.
|
||||
|
||||
For the current three-map design let G be complete global-plus-scoped names,
|
||||
H typed declaration headers, O retained lowerer occurrences, E expression sites,
|
||||
and P nonroot expression sites. Added retained storage is entry slots
|
||||
`G + (H + O) + E + P`, occurrence slots zero, and text bytes the sum of copied
|
||||
names' UTF-8 lengths. The prior baseline still contains E raw expression entries
|
||||
and O occurrence slots. One Path Step is allocated per nonroot expression; any
|
||||
additional retained representation requires an explicit additional reservation.
|
||||
|
||||
Peak scratch additionally includes owner map H, physical-token set H+O,
|
||||
arena-position/path/depth vector E, call-site map C, borrowed reverse-name maps
|
||||
G, syntax-site lookup E and live iterative walk slots W. Count any allocated
|
||||
file/declaration adapter table too. Drop owners/physical tokens after occurrence
|
||||
validation, and paths/calls after argument-child validation before syntax-edge
|
||||
work where possible; otherwise sum their overlapping reservations. A popped
|
||||
work item releases one logical slot. The scalar visited count consumes no
|
||||
allocated entry per visited node. Path-node reservations survive dropping the
|
||||
temporary vector while retained map keys still share those nodes.
|
||||
|
||||
Budget errors abort construction. Existing depth checks may update deepest-frame
|
||||
usage; this is not reparsing or Exact-work recharge. Warning retention and prior static admission
|
||||
metadata stay unchanged. Neither indexing nor failure recovery repeats complete
|
||||
model admission or constructs replacement source. Rejection and destruction must
|
||||
retain the existing bounded-stack policy; no production depth128 enablement is
|
||||
implied by a narrow private test.
|
||||
|
||||
## Required evaluators before implementation acceptance
|
||||
|
||||
The [independently enumerated rich-fixture oracle](analysis-mixed-index-20260924/ORACLE.md)
|
||||
provides concrete names, keys, spans and retained-storage totals. Parent raw-byte
|
||||
verification passes all 36 original and 36 transformed Unicode/CRLF spans using
|
||||
the adjacent `check_oracle_bytes.py`, without running the parser, lowerer, index
|
||||
or compiler. This validates the test data's byte arithmetic, not implementation.
|
||||
|
||||
1. Hand-enumerated OA and SA mixed bundles: entire name catalog plus every typed
|
||||
declaration/reference/expression key, full-file span and spelling. Include
|
||||
interleaved ordinary declarations, cross-file references in both semantic
|
||||
directions, Unicode/CRLF and coincident offsets. The expected index must not
|
||||
be generated by the builder, formatter or lowerer under test.
|
||||
2. Reuse the complete-Project repeated/reordered-call fixture with shared
|
||||
parameter UUIDs in distinct calculations; compare all qualified labels and
|
||||
structural paths, including repeated identical expression text.
|
||||
3. Internal corruption controls: swapped file ownership, wrong owner/field,
|
||||
parent cycle/forward edge, structurally wrong but internally consistent child,
|
||||
duplicate physical token, missing/extra expression, duplicate occurrence,
|
||||
wrong call label/target, missing required argument label, and wrong token
|
||||
classification/spelling. Every control must reach its intended existing
|
||||
structural/token check rather than an unrelated earlier parse refusal.
|
||||
Complete reference occurrence emission remains an invariant of the sealed
|
||||
lowerer, tested by the independent exhaustive oracles in items 1 and 2.
|
||||
The existing syntax-edge checker verifies the expression arena; it does not
|
||||
prove all parameter-declaration or non-expression type-reference occurrences
|
||||
are present. Do not claim arbitrary omitted occurrences are runtime-rejected
|
||||
or introduce an unreviewed duplicate full syntax-reference verifier.
|
||||
4. Hand-counted live-storage cap and cap-minus-one cases: retained baseline plus
|
||||
names/paths/maps, peak overlapping scratch, copied-name bytes, and simultaneous
|
||||
occurrence representations. Check unchanged parse/numeric counters and exact
|
||||
intended refusal phase; no success by clearing the baseline.
|
||||
5. Read-only provenance and lifecycle: exact entry/path/file/byte retention,
|
||||
unchanged whole Project/warnings/admission metadata, no old-updater-compatible
|
||||
conversion, and no usable failed result. Exercise complete success/drop and
|
||||
representative late rejection/drop on the existing small-stack harness.
|
||||
6. Existing typed-only index/update/export tests and full inherited compatibility
|
||||
dispositions must remain accounted for after shared-helper extraction.
|
||||
Strict core Clippy and current-source checks remain required. Private full
|
||||
library source8 failures are not to be suppressed or called passed.
|
||||
|
||||
After this dependency, integrate real ordinary and typed original-coordinate
|
||||
edit producers, combined preflight/emission, output recompile/admission and
|
||||
whole intended-Project equality. Extend to structural edits, identity changes,
|
||||
missing-ID stabilization and checked-save/history before claiming the required
|
||||
editing parity. This proposal does not authorize a smaller final product.
|
||||
@@ -0,0 +1,61 @@
|
||||
# Mixed typed-index seam: independent source/design critic
|
||||
|
||||
2026-09-24. Source-only review of clean private source `aaec78fcc9b273d1485497126392afdd1fb2e54e`; source paths below are relative to `crates/oa-core/src/`. Read root mixed-Project contract section 7, original-span proposal and accepted review, and parent's draft `harness/analysis-mixed-index-contract-20260924.md`. The reviewer performed no compiler, source/build/Gitea mutation, or runtime check. Parent subsequently verified and resolved the findings and adopted the revised [contract](analysis-mixed-index-contract-20260924.md). Initial findings and final disposition are preserved below. Prerelease #37 is unaffected.
|
||||
|
||||
## Assessment and two precise draft refinements
|
||||
|
||||
The consumed wrapper plus shared private verifier is the smallest sound next slice. It preserves original complete-bundle authority without claiming an ordinary occurrence index or enabling editing. Separate mixed verifier duplication offers no demonstrated benefit over narrow borrowed-input extraction. Parent's draft is implementable after settling these two points explicitly:
|
||||
|
||||
1. **Set accounting dimensions in the contract, not during implementation.** Existing mixed-Project contract section 3 assigns map/vector/work entries and expression sites to entry slots; occurrence slots cover lowerer occurrence records. Consequently every new index-map entry, including an occurrence-map entry, charges `entry_slots`; retained `lower::Occurrence` records keep their existing `occurrence_slots`. Owned copied names charge text bytes separately. Parent draft lines 115–119 currently leave this choice open. Applying the old `Index::insert` local `lowering_occurrences(map.len())` check would not satisfy that inherited aggregate policy (`analysis_source/index.rs:643`).
|
||||
2. **Separate runtime damage checks from completeness by sealed origin.** Parent draft lines 138–144 list missing/extra occurrences among corruption controls. The existing verifier does not independently enumerate all expected syntax references: its own module header states that occurrence completeness trusts the lowerer (`index.rs:1–5`). `check_syntax_edges` only traverses expression structure (`:478–632`). Deleting an isolated parameter-declaration occurrence or a non-expression type reference can leave all runtime checks satisfied. Do not promise arbitrary omission rejection without adding a separate reviewed reference-syntax verifier. For this minimal slice, retain sealed parse/lower completeness, use hand-enumerated full occurrence output as the independent oracle, and restrict mandatory runtime omission rejection to checks actually present (missing expression, missing call label for an existing argument child). This is not a reason to widen scope or expose fabrication APIs.
|
||||
|
||||
## Concrete ownership/API candidate
|
||||
|
||||
A private constructor consumes `ResolvedMixedProject<'s>` and returns `IndexedMixedProject<'s> { resolved, data }`. `resolved` owns the unchanged accepted Project, syntax, warnings, admission metadata, raw sites and Ledger (`mixed/project.rs:16–32`, `:535–543`). `data` owns names and the typed expression/occurrence maps. Read-only methods return views borrowing the wrapper; a view may expose canonical file/path/source lookup and qualified keys, but no mutable fields, raw-site constructor or old `Index` conversion. No extra lifetime is needed for self-references because `data` owns its copied name Strings and keys rather than borrowing fields of `resolved`.
|
||||
|
||||
During construction, narrowly split borrows of retained immutable syntax/project/sites, mutable Budget (for the existing depth check), and mutable Ledger. These borrows exist only inside the builder and end before returning the owner. The retained `SourceBundle` borrow outlives the wrapper and prevents safe mutation while it is live; identical lifetimes alone are not provenance, so neither source nor file table is a separately supplied parameter. Keep the constructor in the witness's module boundary or expose only a constructor-authenticated private view to the shared verifier.
|
||||
|
||||
Do not reconstruct `BTreeMap<usize, String>` from mixed files. `ParsedMixedSyntax.files` already fixes canonical ordering, original source strings and tokens (`mixed.rs:36–58`, `:370–376`). A borrowed file lookup can return `(original_source, token_slice)` for either existing typed-only inputs or the mixed file table. Typed declaration iteration should yield `(file_key, &Declaration)` over retained vectors; site inputs are borrowed expression/occurrence slices. Avoid allocating cloned `Unit` declarations, relexing, rebuilding Lowered model maps, rerunning admission or generating replacement source.
|
||||
|
||||
The old nominal `Index<'s>` remains the only input to `Plan` and `Composition` (`update.rs:15–27`, `:238–245`). A shared private `IndexData`/verification implementation does not justify a generic update trait, `Deref<Index>`, `AsRef<Index>`, or a public adapter. Keep the old `sources: &BTreeMap<usize, String>` authority in its wrapper (`index.rs:122–128`) and preserve typed-only checks/semantics. Mixed-only aggregate reservations must not be accidentally imposed on legacy tests via an unreviewed policy change.
|
||||
|
||||
## Reuse boundary and scope
|
||||
|
||||
Reuse structural key/path semantics (`index.rs:15–120`), owner/physical-token checks (`:246–283`, `:635–675`), expression-parent/path construction (`:288–359`), qualified roles and call-label checks (`:360–472`), and original-syntax expression-edge verification (`:478–632`). Extract these algorithms with small private input/storage adapters, rather than introduce a public generic indexing framework.
|
||||
|
||||
Replace the current name-construction assumption only for mixed input. It currently enumerates ordinary identities and then separate Lowered definitions/elements (`index.rs:209–241`); mixed resolution already moved those models into the complete Project (`mixed/project.rs:439–459`). Iterate every complete Project global once, then scoped parameters once. Global categories include Project, definitions, elements, relations, diagrams and placements, artifacts, and traces (`model.rs:1763–1783`). A streaming borrowed walk avoids `Project::identities()`'s temporary Vec; otherwise reserve that Vec before calling it. Names include ordinary globals for resolution/namespace purposes, but indexed declarations/references remain typed-owned. Ordinary references to typed targets are preserved in syntax/model but not supplied by this view.
|
||||
|
||||
A separate mixed-index implementation would duplicate roughly the whole path/call/syntax-edge verifier and its damage tests; later fixes could diverge. Reconsider only if extraction exposes an actual incompatible invariant, rather than because mixed fields require private access. The distinct wrappers already express the genuine provenance difference.
|
||||
|
||||
## Aggregate storage inventory and release points
|
||||
|
||||
Continue `Ledger::reserve`'s checked totals and inherited Budget (`mixed/project/accounting.rs:43–70`), including retained syntax, warnings, expression sites and occurrences. Never start from zero. One consumed owner prevents repeated retained indexes using one accounting baseline.
|
||||
|
||||
For a successful simple implementation let G be complete global-plus-scoped name count, H typed headers, O retained lower occurrences, E expression sites, P nonroot expression sites, and C calls. The added retained logical storage is **entry slots = G + (H + O) + E + P; occurrence slots = 0; owned text = sum of the copied names' UTF-8 lengths**. The existing baseline already contains E raw-site entry slots and O raw-occurrence slots. P counts each allocated shared Path Step once; Arc clones do not allocate another Step but their map/vector records still count. This formula assumes exactly the current three maps and one Step per nonroot expression; any added representation must be listed separately.
|
||||
|
||||
Scratch must additionally reserve before growth:
|
||||
|
||||
- typed owner map H; physical-token set H + O;
|
||||
- arena-position/path/depth vector E; call-site map C;
|
||||
- borrowed reverse-name maps G; syntax-site lookup E; iterative syntax work slots W;
|
||||
- any allocated file/declaration view table, error mapping retained in success, or identity-visitor scratch.
|
||||
|
||||
Borrowed reverse-name strings cost no new text bytes. Work-pop releases its logical slot; this remains a logical live-element policy, not retained Vec capacity/RSS metering. Scalars such as `visited` are not a separately allocated entry per visited expression.
|
||||
|
||||
Explicitly drop/release owners and physical-set storage after occurrence validation, then paths/calls after argument-child validation and before syntax-edge work where possible. Otherwise all remain live and must be summed at that peak. Retained Path Steps remain charged even after dropping the temporary path vector, because expression/occurrence maps share them. Failure drops the entire consumed owner; no partial reusable success or stale reservation lease escapes. Preserve iterative Path destruction (`index.rs:68–79`). `Path::edges` allocates a Vec (`:82–90`) and currently has test-only call sites; do not turn it into a new unmetered production materialization API.
|
||||
|
||||
## Discriminating acceptance cases
|
||||
|
||||
1. Independently enumerate complete names and all typed key/location/spelling tuples in OA and SA mixed files with Unicode/CRLF, ordinary prefixes and coincident offsets across canonical paths. Compare every tuple and original whole bundle, Project, warnings and admission metadata; do not generate expectations by the builder/lowerer. Include a parameter UUID equal to a global and reused in two calculations.
|
||||
2. Reordered repeated calls must retain distinct parent paths, correct callee-scoped labels and original argument order. Corrupt a label to an existing other callee with the same parameter UUID/spelling so token/catalog checks cannot mask the intended call-site rejection (`index.rs:426–469`).
|
||||
3. Runtime negatives: missing/extra expression; forward/cyclic parent; cross-file/owner/field location; internally consistent but syntax-wrong child edge; duplicate physical token or qualified occurrence; non-Word/partial/UTF-8-split token; missing call label with an argument child. Record which exact check rejects. For arbitrary reference omission, the full authored oracle must fail; do not label this builder rejection unless new syntax completeness logic exists.
|
||||
4. Hand-count a retained baseline plus the formula above and peak scratch: exact cap succeeds, cap minus one fails at the intended reservation before growth. Separate copied-name-byte pressure and overlapping temporary maps. A case where old baseline and new maps each fit alone but their sum does not catches ledger reset; retain both raw and indexed representations throughout. No parse byte/token/entry/reference or Exact-work recharge is permitted; inspect control flow as well as counters. The existing `Budget::frame` may update deepest-frame usage while checking depth (`budget.rs:118–121`); this is not another parse.
|
||||
5. Type/API inspection must establish no old-updater-compatible conversion and no replaceable bundle/table/profile inputs; wrong-session composition is a later compositor negative, not an invented runtime API in this read-only slice. Test original provenance through views and success/late-failure cleanup on the existing small-stack harness. Re-run legacy Index/update/export oracles after extraction.
|
||||
|
||||
## Dependencies deliberately still open
|
||||
|
||||
This slice neither authenticates a complete ordinary occurrence set nor authorizes rename emission. Subsequent work must derive both edit producers and complete rename intent from the same witness/target; define the precise supported ordinary diff/compound-edit normalization; aggregate and validate original-coordinate edits; emit once; recompile/admit the actual complete result; compare the whole independently intended Project; and preserve atomic history/save behavior. Full parity still requires typed subtree and parameter edits, repaired deletion/reidentity, file movement/ownership, missing-ID stabilization/reindexing, migration/public dispatch, and editor/root reconciliation. These remain delivery requirements, not reasons to expand this next read-only dependency.
|
||||
|
||||
## Revised parent draft disposition
|
||||
|
||||
Re-read the updated contract's live-storage and evaluator clauses after parent revision. Both requested refinements are now explicit: new map/vector/set/Path storage uses entry slots while retained lower occurrences keep occurrence slots, and runtime expression/token/call checks are separated from complete occurrence emission guaranteed by sealed lowering and independently enumerated output tests. Those changes resolve the two design ambiguities identified above. No remaining blocking source/design finding for this bounded implementation proposal. The concrete retained-storage formula and failure cases in this report are implementation guidance, not measured results or a final accepted decision. Parent retains adoption and gate authority.
|
||||
@@ -0,0 +1,91 @@
|
||||
# Next private mixed-parser slice
|
||||
|
||||
**Original proposal, subsequently narrowed by the [independent review](analysis-mixed-parser-review-20260923.md).** The parent accepted the private syntax-only direction with that review's corrections; implementation and runtime acceptance remain open. In particular, the review resolves the base to A and corrects this proposal's inference of ordinary grammar drift from whole-file statistics. The following inspection is retained as its original reasoning, not a public source8 claim.
|
||||
|
||||
Read-only inspection, 2026-09-23. Private source pin A=`10d7f3340d5cd61a656dc0bcf7c143f8897c5551`; current ordinary/public pin R=`195ddbbba5b352af09bca385a6c3e4dd1c77f8c8`. Both pins checked before and after inspection. No compiler, source edits, GUI or Gitea operations. Parent reports A passed Windows 173 listing/65 typed/strict; those gates do not exercise mixed parsing.
|
||||
|
||||
Read the published `harness/analysis-original-span-proposal-20260922.md` and `harness/analysis-original-span-review-20260923.md` at R, plus A's `harness/analysis-source8-20260913/analysis-source8-repair-design-r2-20260913.md`, especially “Source pipeline and exact editing” and the resource/older-version contracts. This note narrows their next parser dependency; it does not replace them.
|
||||
|
||||
## Recommendation and exact deliverable
|
||||
|
||||
Implement **one private complete-bundle syntax parse**, producing an opaque `ParsedMixedSyntax` witness. It retains ordinary unresolved syntax and ordinary edit positions alongside typed unresolved declarations, from one significant-token stream per original file and one inherited budget for the complete pass. Start with explicit stable IDs only, source8 entry, existing ordinary grammar, all existing typed declaration grammar, and normal include/path rules. It returns no Project, Index, rename authority, output text or installed document. A complete semantic/session witness is a dependent slice, not an alternative name for this syntax result.
|
||||
|
||||
This resolves actual mixed-file dispatch, full-file coordinates, lexical ownership, aggregate scanning and same-parse ordinary edit provenance before undertaking cross-domain lowering or production composition. Root public dispatch remains closed; a private constructor selects source8 policy before scanning and verifies that the actual entry says 8. No schema constant, public parser acceptance, save route or model change belongs in this patch.
|
||||
|
||||
## Existing source seams
|
||||
|
||||
Paths below are repository-relative; line numbers belong to their indicated pin.
|
||||
|
||||
| Existing site | Consequence for the slice |
|
||||
| --- | --- |
|
||||
| R `language.rs:1066` `parse_unit`; `:1272` Parser; `:1489` unit | `strict_tokens` owns a separate token Vec; unit mixes entry/header/includes, iterative ordinary declaration nesting and fields. Add a private mixed entry through factored grammar operations, retaining the iterative declaration-owner stack. Public versions currently stop at 7. A has the corresponding unit at `:1332`. |
|
||||
| R `language.rs:1155–1270` `edit_declarations`/`edit_positions` | `edit_declarations` reparses and `edit_positions` rescans, derives delimiter ends and assumes token shapes. A mixed caller cannot invoke these helpers on the original mixed file. Factor a positions projection over the already parsed ordinary AST and retained token/span metadata. Keep the legacy wrappers' observable behavior. |
|
||||
| R `language.rs:1392` ordinary `value_inner` | Ordinary numbers remain finite JSON values; they are not Exact. Raw `-` + number must be consumed as one ordinary signed value with its complete span; typed `1-2` is subtraction. Preserve ordinary rejects such as `+1`, `1e`, `1_0`, `0x1` and nonfinite JSON numbers. |
|
||||
| A `language/raw_tokens.rs` Scanner and legacy adapter; `analysis_source/lex.rs:23` tokens | Scanner already preserves raw full-buffer spans and separates typed operators. Legacy adaptation greedily groups adjacent numeric parts and performs JSON classification. Do not use that whole-file legacy adapter for mixed source; typed lex currently charges bytes/raw tokens, including trivia, before retaining significant tokens. |
|
||||
| A `analysis_source/cursor.rs:5` Cursor; `declarations.rs:243` parse | Typed parse already consumes a borrowed significant cursor, checks inherited version=8, rejects lexical owners, and returns unresolved full-buffer declaration/field/reference/literal spans. This is the useful dispatch seam; `analysis_source::parse` is an expression-only whole-input wrapper. |
|
||||
| R `source_project.rs:303–605` compile_once | Existing iterative include visitor owns path normalization, cycle/depth/edge/visited checks, aggregate ordinary counts, unreachable-file rejection and located errors. Reuse/factor its traversal policy for private syntax collection; do not call compile_once and discard its model. Its `:429` parse_unit call is the later production substitution point. |
|
||||
| A `analysis_source/index.rs:147–195` parse/build | Current parse scans typed-only numeric-keyed files and accepts a separately supplied ordinary Project. Calling it on mixed buffers fails; calling it on extracted typed text invents provenance. Keep build private. This slice does not manufacture an Index. |
|
||||
| A `analysis_source/lower.rs:269` lower | Accepts ordinary skeleton plus all typed units; collects all typed globals/signatures before bodies; deliberately does not admit the skeleton. Needed in the next semantic slice after the shared syntax witness exists. |
|
||||
| R `source_project.rs:895`, `:918`, `:984`, `:1050–1071` update path | Actual-file ordinary edit ASTs, formatted comparison ASTs and local project-header spans have different roles. Later production editing uses retained actual-file positions; retain CompleteForEdit/Compact formatting and their comparison semantics. Scalar application, structural second pass and full candidate equality remain outside this parser patch. |
|
||||
|
||||
R and A's ordinary files are materially different (427 insertions/81 deletions across language.rs/source_project.rs in the inspected comparison). R also contains current CompleteForEdit/Compact work. Do not copy A's whole language/source_project files over R or overwrite the parent's Scenario integration. Pin the combined implementation base and agree ownership of the shared grammar hooks before coding.
|
||||
|
||||
## Minimal private interfaces and ownership
|
||||
|
||||
Names are illustrative, not a request for a public framework:
|
||||
|
||||
```rust
|
||||
parse_mixed_syntax(bundle: &SourceBundle, limits: ProjectLimits,
|
||||
test_profile: /* optional tighter private profile */)
|
||||
-> Result<ParsedMixedSyntax<'_>, Vec<SourceDiagnostic>>;
|
||||
```
|
||||
|
||||
The result privately borrows the **actual complete SourceBundle**, owns the one finished Budget, canonical file table, parsed units and retained significant tokens/ordinary position metadata. It retains entry, every canonical path and original bytes through that borrow. File keys are minted once from canonical bundle keys before traversal; the include visitor visits each reachable file once. A FileKey is meaningful only through its owning result, never an independently accepted `(usize, path)` mapping. Store paths/source references into the borrowed bundle rather than a self-referential owned Index-plus-source-map structure. Constructors are private; expose read-only inspection only. This establishes syntax provenance, not semantic validity or mutable-current freshness.
|
||||
|
||||
Each file result contains its source reference, entry/fragment role, include spans, ordinary declarations with lexical owners, typed declarations, and ordinary edit positions. Positions remain half-open **full original file UTF-8 byte offsets**, even after Unicode comments, the project header, includes or preceding declarations. No header/include offset is added to typed spans. Nested ordinary declaration spans legitimately contain descendant declarations; declaration ranges are descriptive ownership, not automatically disjoint replacement authority.
|
||||
|
||||
Use one shared significant-token cursor state. Ordinary grammar methods may have a domain-specific borrowed view; they must advance the same token position used by typed `declarations::parse`, without copying/rescanning a declaration or replaying a speculative parse. Preserve legacy grammar through factoring, not by cloning a second independently maintained ordinary parser. Keep token access/classification adapters small and local; no general parser trait framework is required.
|
||||
|
||||
At document scope, dispatch exact typed declaration words before generic ordinary `declaration_kind`. Inside an ordinary element's open body, recognize a typed header only to deliver the existing “must be roots” error; do not reinterpret it as an ordinary field. Contextual words such as `unit`/`calculation` must remain legal aliases where already allowed. Entry descriptions/includes keep current placement rules. Generic derived long analysis kind names must not become an alternative accepted syntax.
|
||||
|
||||
For ordinary edit positions, a raw numeric value may cover several significant tokens (`-12.5`). The current token-end heuristic would cover only `-`. Record ordinary value end spans as grammar consumes values (a small per-file side table keyed by the existing AST start offset is sufficient), then project EditValue from that AST plus spans. Keep the shared legacy/migration Ast representation unless a demonstrated need justifies changing it. Record ordinary header token roles from actual parsing as well; do not obtain them with another `edit_header` scan. Typed declaration spans/expressions never enter ordinary EditKind::Record recursion.
|
||||
|
||||
**Stable-ID scope:** reject omitted project, ordinary global, typed global or parameter IDs in this first private entry before returning a witness. The ordinary `optional_id` path currently creates UUIDs, so the private mode must reject before that generation; typed IdentitySyntax already retains missing identity/insertion information. This intentionally postpones stabilization instead of assigning hidden IDs or granting edits against pre-insertion offsets. Later accept missing IDs only through bounded insertion into the returned full documents and a new complete parse of those actual bytes.
|
||||
|
||||
## Budget and parser invariants
|
||||
|
||||
Create one Budget at the private complete-parse boundary from source8 hard ceilings intersected with caller restrictions and any tighter test profile. Own it in the returned witness; lower/index/compose later obtain it from that witness, with no ambient replacement/reset API. Creating a Budget for each include or switching token policy after seeing local fragment syntax is invalid. Selecting explicit private source8 mode before lexing avoids a public version-probe implementation in this slice; eventual public dispatch needs a bounded header read with unambiguous accounting.
|
||||
|
||||
Precheck aggregate file/UTF-8 byte size before broad token/String allocations. Charge each file's original bytes once in scanning; a prior noncharging aggregate limit check is not another charge. Charge every raw token including trivia once, before storing; significant filtering and ordinary legacy-compatible numeric consumption do not recharge tokens. EOF carries no token charge. Do not scan or convert typed exact atoms again to derive positions. JSON ordinary values keep their finite numeric domain, and exact numeric WorkMeter use belongs only to typed numeric consumption.
|
||||
|
||||
The mixed parser must also bound ordinary temporary containers/references before allocation; existing per-unit graph counts and final `check_model` alone are insufficient. Specify the charge table in the patch: ordinary declaration/field/value/list-entry allocations join the private syntax-storage ceiling; retain caller graph/record accounting separately; ordinary reference storage is bounded before copying, while caller ordered-step/reference semantics remain explicit rather than silently relabeling every scalar reference a model step. Typed existing entry/reference charges stay intact. Count aggregate phase metadata (file table, positions, catalogs later) before reserving; the existing typed budget phase checks are reusable but do not magically bound ordinary additions. Preserve ordinary value-depth64 and separate typed frame256/logical-depth128 semantics and safe failure cleanup. Tests must discriminate which bound fails.
|
||||
|
||||
## What this does not yet solve
|
||||
|
||||
The dependent **ResolvedMixedBundle** must consume this syntax witness, collect one global alias/identity/category catalog including typed declarations, build the ordinary skeleton against it without premature complete-model admission, lower typed units once, merge them, admit the whole Project, and construct Index internally from the same tokens/units/lowerer result. Only that result can bind complete Project, Index, file table, original bundle and inherited Budget for composition. Current A Index borrows `BTreeMap<usize,String>`; adapting its private source accessor to witness-owned file references is justified there. Exposing `build(Lowered, spans)` to arbitrary callers is not.
|
||||
|
||||
R `language::resolve_declarations` combines ordinary construction, version migration and final limits; A additionally migrates source7 through `migrate_v7`. Neither is an appropriate “parse ordinary and admit first” shortcut: forward ordinary references may target typed records, and typed subjects/types/calls may target later files. The semantic slice needs a narrowly extracted non-admitting ordinary construction step, complete catalog, and one final model admission. Syntax witness tests cannot prove these relationships or satisfy intended-Project equality.
|
||||
|
||||
Coordinated version7→8 blockers for production are: reconcile root schema7 with private schema8 definitions/analysis and Scenario payload; preserve frozen older DTO/source gates and the source3 old-envelope boundary; choose the one public dispatch/migration change; stabilize missing global/parameter IDs and reindex actual returned bytes; retain required categories and ordinary complete/compact edit behavior; supply complete mixed lowering/index provenance and supported-edit diff gate; compose original edits once and recompile/admit/compare the entire actual output; pass migration/history/checked-save and resource calibration gates. None requires a new metamodel or blocks this private explicit-ID syntax slice. Root should coordinate them, not accept `arcadia 8` early to make a fixture pass.
|
||||
|
||||
## Rejected shortcuts and cost comparison
|
||||
|
||||
| Approach | Decision |
|
||||
| --- | --- |
|
||||
| Shared scanner/cursor + existing grammar extraction + same-parse ordinary positions | Recommended: moderate localized factoring; directly tests mixed boundaries and retains ownership. Limit deliverable to syntax until semantic construction is ready. |
|
||||
| Parse ordinary and typed projections independently, then combine models/indexes | Reject: masking preserves numeric offsets at best but loses actual-source token/ownership proof; substring/concatenation creates local offsets; separate parses reset or double-charge resources and cannot establish cross-domain global resolution. A clever remap does not repair these invariants. |
|
||||
|
||||
Also reject whole-file legacy tokenization (typed punctuation fails/typed number domain changes), silent retries with larger budgets, counter subtraction after rescanning, caller-supplied numeric file remapping, and treating equal bytes/shared lifetimes as proof of the same semantic parse. A full replacement edit AST is unnecessary for this step.
|
||||
|
||||
## Six observable first-slice fixtures
|
||||
|
||||
All are proposed, unrun. Use independently authored source literals and expected span/substring tables rather than expectations extracted from the result itself.
|
||||
|
||||
1. **Interleaved CRLF/Unicode:** entry description/comment, ordinary nested element, typed calculation with `1-2` and `1e-2`, ordinary record with negative coordinates after it. Verify exact original substring at every selected ordinary/typed/header/value span, lexical owners, and byte-identical complete input. Explicit `-12.5` ordinary span must include the minus and full atom. No output Project claim.
|
||||
2. **Complete include binding:** root + nested-path module + shared dependency reached twice; mixed declarations after include prefixes in two files with identical local offsets/spellings. Check unique scan count, fixed FileKey/path mapping, exact include spans; cycle, missing, escaping and unreachable-file negatives return no complete witness. Entry/path/map replacement is not an accepted constructor operation.
|
||||
3. **Dispatch and grammar separation:** `unit`/`calculation` used as allowed ordinary aliases, all short typed declaration kinds, typed roots between nested ordinary blocks. Reject typed lexical nesting, long derived analysis names, fragment version headers and source7 at this private source8 entry. Existing public source3–7 and public source8 rejection remain unchanged.
|
||||
4. **Number/position compatibility:** ordinary signed/exponent JSON values and typed exact ratio/scientific expressions, with comments between legal typed tokens. Ordinary `+1`, split-sign forms not admitted by the old grammar, malformed/nonfinite JSON values reject. Typed values beyond JSON precision retain exact values/literal spans. Existing ordinary token compatibility tests plus independently stated old outcomes prevent broadening by accident.
|
||||
5. **Aggregate budget and failure cleanup:** one-file and split-file variants with exact byte/token/ordinary-storage/reference/numeric ceilings and plus-one failures; include trivia; add an earlier ordinary allocation so a later typed declaration crosses the shared ceiling, and reverse producer order. Previously latched failure returns no witness. Test-only scanner visitation/Exact conversion counters demonstrate one scan/one conversion; unchanged counters alone do not. Run retained 128-KiB typed depth/rejection cleanup controls when parser ownership changes warrant them.
|
||||
6. **Opaque syntax scope:** explicit-ID positive across all declaration/parameter kinds; remove one ID in each domain and require refusal with original bundle unchanged. Read-only ordinary EditValue and typed occurrence syntax inspect the same witness; no external AST/Lowered/FileKey-map constructor exists. Unresolved forward ordinary/typed references can be retained syntactically, but do not assert they resolve. A following semantic slice must add reordered cross-file forward references, repeated/reordered calls, reused parameter UUIDs, catalog collisions, wrong-session batch rejection and independent whole-Project equality.
|
||||
|
||||
First patch completion means these syntax/ownership/resource fixtures and unchanged ordinary compatibility tests pass on an immutable Windows source pin with fresh review. It does not mean mixed renames, source8 save, full S3/S4, public schema activation, or all semantic references work.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Private mixed syntax proposal: independent review
|
||||
|
||||
**Disposition: proceed with a private syntax-only implementation on A=`10d7f3340d5cd61a656dc0bcf7c143f8897c5551`, with the bounded corrections below.** Do not first merge the full current root or copy either side's complete language/updater files. The proposed single-buffer cursor, original coordinates, complete-bundle borrow and inherited budget address the real next dependency. A semantic witness, compositor, ID stabilization and public activation remain separate dependent work.
|
||||
|
||||
Reviewed `.local/analysis-mixed-parser-next-slice-20260923.md`, the published original-span proposal/review at R=`195ddbbba5b352af09bca385a6c3e4dd1c77f8c8`, the private r2 design, and actual A/R source. Source/design review only: no tests, compiler, GUI, source/worktree edits or Gitea operations. Parent's reported A gates are export/typed evidence, not mixed-parser evidence. Line references below are pinned to A or R; `analysis_source/*` means `crates/oa-core/src/language/analysis_source/*`.
|
||||
|
||||
## Resolve the implementation base now
|
||||
|
||||
The proposal's 427/81 whole-file difference is real but does **not** demonstrate materially different ordinary syntax. Direct Git extraction established:
|
||||
|
||||
- `source_project.rs` is byte-identical from the file start through the entire include/compiler/edit-application section before `format_bundle`. Thus the traversal policy at R/A `:303–605` has no R/A drift.
|
||||
- `parse_unit` and the edit-header/declaration/position section are byte-identical (A `language.rs:905–1109`, R `:1066–1270`). Parser `fields` through `value_inner` is byte-identical (A `:1179–1293`, R `:1336–1450`). The iterative `unit` implementation differs only in A initializing its additional empty `Project.definitions` member. UUID optional-ID behavior is equivalent; R factors the existing shape check into `has_uuid_header_shape`.
|
||||
- Real differences requiring later integration include raw-token versus current bounded tolerant-outline tokenization, A's analysis model/migration/source3 bridge, and R's ordinary defaults plus complete/compact formatter/updater. R `language.rs:1757` and `source_project.rs:853` are semantic/edit behavior, not prerequisites for a syntax-only result. A's generic `declaration_kind` also sees a larger `Kind::ALL`; equal grammar function bodies do not imply equal recognized kind sets.
|
||||
|
||||
**Concrete base:** keep A's tested private analysis/numeric/export dependency closure and existing module isolation; factor its ordinary syntax/position code in place, using R's pinned ordinary behavior as a compatibility oracle. Implement the mixed unit/bundle owner privately, sharing the factored ordinary grammar and include policy with existing A paths. Do not port R's whole updater, Text outline, Scenario payload or schema constants for this experiment. Keep a narrow compatibility note identifying the equal syntax seams and intentionally deferred semantic/editor differences. Recheck those seams against the parent's eventual integrated pin before proposing production composition. This avoids an unrelated model/GUI integration prerequisite and does not claim A already contains root ordinary default/update behavior.
|
||||
|
||||
## Corrections and acceptance conditions
|
||||
|
||||
1. **Define ordinary numeric consumption precisely.** A `raw_tokens.rs:158–181` preserves the legacy greedy, *byte-adjacent* numeric alphabet; the mixed significant adapter (`lex.rs:19–51`) discards trivia. A two-token `-` plus Number rule without adjacency would newly admit `- 12.5` and `- //comment\n12.5`. Require contiguous original spans, complete ordinary numeric spelling, finite JSON classification, and the original complete value end. Keep `+1`, `.5`, `1e`, `1_0`, `0x1`, `1-2` invalid as ordinary values; typed `1-2` and exponent `1e-2` retain their different meaning. This belongs in a small ordinary-consumer view over the same cursor, not a second lexer or substring parse. Full field/value spans must include semicolon/delimiters correctly after multi-token negatives. Check nested records/lists and `@ /*not supported*/`-style malformed trivia as well as top-level coordinates. Only `//` comments are supported by the current scanner.
|
||||
|
||||
2. **Preserve occurrence ownership before semantic uniqueness.** Explicit IDs are a reasonable first-slice restriction because the ordinary AST and lexical owners currently require `Id`, and edit positions refuse unstabilized declarations (R `language.rs:1232–1240`). Reject missing ordinary/project IDs before `optional_id` generates UUIDs (`:1316–1319`); inspect typed globals and every parameter's existing `IdentitySyntax` (`A declarations.rs:34–42,406–434`). This is staging policy, not proof syntax intrinsically requires explicit IDs. Do not add stabilization here. Conversely, do not key syntax/position storage solely by UUID or alias: ordinary `edit_declarations` explicitly preserves occurrences through temporarily duplicate identities (R `:1151–1154`), and global uniqueness is deferred to resolution. Preserve ordered declaration occurrences and lexical owner occurrence information; either retain duplicate IDs without collapsing nodes, or explicitly label any early uniqueness rejection as additional syntax-slice policy. Reused parameter UUIDs across calculations remain legal. The side table keyed by actual AST start is suitable.
|
||||
|
||||
3. **Make the storage table concrete without relabeling model limits.** A `budget.rs:16–24` currently derives syntax entries from caller `max_records` and references from `max_step_references`. Counting every ordinary declaration, field, value and list slot against that same limit can reject a one-project/one-element input under `max_records=2` solely because it has a description field, although the ordinary model has two records. Similarly `parent @x` is not automatically an ordered model step. Keep bounded syntax storage and aggregate ordinary model counts distinct and document which bound each fixture targets. If retaining the existing candidate source8 syntax ceiling is deliberately stricter, state that private policy; do not claim preservation of all caller-limit behavior. Preserve public source3–7 accounting. Tokens/metadata/position copies need an aggregate storage bound, not a fresh per-file check or repeated independent allowance. Do not charge the same AST record again merely because a span references it. A `lowering_entries/index_bytes/lowering_occurrences` are noncharging **phase checks** (`:137–160`) and do not latch failure; all their errors must propagate immediately. `Budget::active` alone is not proof those phase failures were retained.
|
||||
|
||||
4. **Keep factoring localized and dispatch contextual.** Share `fields/value/nesting/header` operations; retain the iterative declaration stack. The ordinary parse wrapper can continue selecting its legacy view and the private mixed wrapper its raw-token view, while both call one grammar implementation. No generic parser trait framework or second independently maintained ordinary parser is needed. Inside an ordinary body, reject a *typed declaration header*, not every field whose word happens to be `unit` or `calculation`; ordinary syntax retains generic field names before semantic validation. At document scope, short typed words dispatch first. Explicitly exclude long analysis-derived ordinary kinds on A: `language.rs:236–259` builds names from the expanded `Kind::ALL`, so dispatching short words first alone does not close that alternative syntax.
|
||||
|
||||
5. **Provenance is the whole constructor, not token retention alone.** Borrow the actual `SourceBundle`, including entry and all files, validate canonical/case-colliding paths and complete reachability before returning, and mint the path table internally. Maintain one shared cursor index and Budget. Temporary borrowing of the owned token vectors is sufficient; do not create a self-referential stored Cursor. Retaining tokens is justified for the later Index's token witnesses (`A index.rs:185–195`), but there is no need to clone a second token stream or expose a public profile/configuration framework. A test-only tighter-profile constructor or fixed private helper suffices. A result may privately reuse the existing header container with its empty Project shell, but must not expose it as an admitted ordinary model. The distinction between `ParsedMixedSyntax` and a later resolved/admitted witness is sound. Keep construction of the latter internal, with ordinary construction against the full catalog, lower-once, final admission and same-parse Index creation; the current typed-only `Index::parse(ordinary, numeric_files, budget)` is not that authority.
|
||||
|
||||
## Bounded first-patch gates
|
||||
|
||||
The six proposed fixtures are a useful sufficient scope with these additions: signed-value adjacency and nested value/field ends; a contextual typed word used as an ordinary field/alias; long derived analysis-kind refusal on A; duplicate ordinary identities without position collapse; reused scoped parameter UUIDs; ordinary model-record versus syntax-storage boundaries; shared dependency scanned once; and exact Unicode/CRLF source substrings independently specified. Missing-ID failures must leave input unchanged and never call UUID generation. A deliberate phase-bound failure must return no witness, even though that check does not latch the Budget.
|
||||
|
||||
Require compatibility tests for the public legacy token/parse paths actually touched by factoring, including the existing `raw_tokens::tests::legacy_adapter_retains_authored_numeric_operator_and_trivia_tokens`. Retain ordinary depth 64, typed frame 256/logical depth 128, and the applicable small-stack cleanup controls. These are future acceptance tests, not executed results. Do not require unrelated source updater/GUI integration gates to approve a syntax-only private patch; require those when its code is composed into the corresponding production path.
|
||||
|
||||
The broader rejection of projection/masking and fabricated `build(Lowered, spans)` provenance is justified. No alternate architecture is needed. The next useful action is this bounded private implementation with the charge table and ordinary numeric adapter specified in its patch, followed by fresh source review and appropriate parser gates.
|
||||
@@ -0,0 +1,90 @@
|
||||
# Private mixed Project contract
|
||||
|
||||
**Accepted design, 2026-09-23; implementation remains gated by syntax acceptance.** This document specifies required behavior and proposed acceptance oracles; it reports no pending gate or fixture as passed. Implementation facts refer to `a578ac98a06772a60bec00ae287e65650680d119`, paths under `crates/oa-core/src/`. The [independent review](reviews/mixed-project-contract-20260923.md) informed the retained-site and diagnostic-allocation requirements below.
|
||||
|
||||
This slice supplies an admitted private Project with retained original-source provenance, a dependency toward the [project goal](../PROJECT_GOAL.md). It extends the [mixed-parser ownership contract](../harness/analysis-mixed-parser-proposal-20260923.md#minimal-private-interfaces-and-ownership) and [review corrections](../harness/analysis-mixed-parser-review-20260923.md#corrections-and-acceptance-conditions), while preserving the [original-span namespace/resource/acceptance invariants](../harness/analysis-original-span-proposal-20260922.md#namespace-resource-and-acceptance-invariants) and [reviewed boundary separation](../harness/analysis-original-span-review-20260923.md#concrete-next-implementation-boundaries). It does not activate public source8 or deliver mixed editing.
|
||||
|
||||
## 1. Result, authority and order
|
||||
|
||||
Consume actual `ParsedMixedSyntax`. A successful opaque result owns the complete Project, mapped warnings, successful static admission limits/usage and retained syntax/lowering provenance. It retains the actual borrowed SourceBundle, exact entry, complete canonical paths and original bytes, canonical numeric-file-key table, exact ProjectLimits and the original Budget. No separately supplied Project, aliases, Lowered, tokens, file remap, replacement Budget or widened limits may establish authority. No mutable Project or edit constructor escapes this read-only boundary.
|
||||
|
||||
Required sequence: complete headers/signatures → ordinary construction once → typed body lowering once → checked disjoint moves into complete Project → retained model limits → one static/architectural admission route → accepted result. No rescanning, Exact conversion, ordinary decoder duplication, source emission/reparse, UUID invention, or migration/admission of a partial skeleton. Missing explicit IDs remain refused by syntax. Failure returns no usable partial witness.
|
||||
|
||||
Seed the entry Project first. Thereafter process canonical file order and original declaration-start order within each file, merging ordinary/typed header occurrences. Detect alias collision before ID collision when both apply, preserving both source owners. Validate complete catalog uniqueness before Project map insertion. Ordinary construction and typed lowering follow this order restricted to their respective declarations; normal validator order stays unchanged. Syntax/include traversal order is unchanged. This is a phase policy, not a promise of earliest physical error across phases.
|
||||
|
||||
Catalog categories include project, ordinary objects, neutral definitions and typed layer roots. Parameter identities/names remain scoped to their calculation; equal parameter UUIDs in different calculations or equal to a global UUID are permitted. Ordinary alias projection derives solely from this complete catalog. Preserve declaration occurrence→file mapping through ordinary conversion errors; UUID lookup cannot distinguish rejected duplicates.
|
||||
|
||||
## 2. Shared construction and retained-site handoff
|
||||
|
||||
Extract the existing `language.rs:1667 resolve_declarations` construction path, including deferred node attachment. Preserve **both Project and legacy_elements scratch** for the existing source3 tail. Existing source3–7 wrappers retain migrations, required fields, defaults, Scenario MixedLegacy fallback, lexical-parent conflict behavior and diagnostics. Public parser/version acceptance remains unchanged. Use borrowed declaration/unit views over retained per-file ASTs, rather than cloned flattened ASTs or a second ordinary decoder. Existing typed-only lower callers retain their wrapper behavior, including diagnostic precedence.
|
||||
|
||||
`lower.rs:113 Lowered` separates model maps from occurrence/expression vectors. Move its typed maps disjointly into Project once; do not retain another typed model. Move these three things into the sealed result **together and unchanged**:
|
||||
|
||||
- the one global `Lowered.expressions` arena shared across all files/declarations;
|
||||
- the paired occurrence vector, whose `expression` indices address that arena, and each expression's parent-index/Child edge;
|
||||
- the canonical numeric-file-key table referenced by both vectors and retained files.
|
||||
|
||||
Do not independently sort/compact vectors, join separately numbered per-file arenas or renumber files. Every owner, field, scoped target and original whole-file span retains its meaning. Preserve actual tokens, typed AST, ordinary ordered positions and lexical ownership beside the sites. Numeric arena positions remain private; they are not persistent IDs.
|
||||
|
||||
This retained handoff has not performed Index's physical-span, call-site or independent syntax-edge verification. A later typed Index must consume the sealed accepted result, derive names/scoped parameters from complete Project once, and verify original syntax/sites without lexing, lowering or admitting the Project again. Neither the existing numeric-map Index nor its updater is changed in this slice.
|
||||
|
||||
## 3. Resource contract
|
||||
|
||||
Keep cumulative parsed bytes/tokens/syntax entries/Exact work unchanged. Ordinary active references add once to the existing reference counter. Preserve caller graph/record/step/file/include limits; complete nested model records remain checked at final model admission. Static analysis work remains its existing separate meter.
|
||||
|
||||
A private auxiliary ledger tracks checked-add live totals `(entry_slots, occurrence_slots, owned_text_bytes)`. Before allocating or copying an in-scope representation, check the **aggregate of all live reservations** through the inherited Budget: `lowering_entries(total)`, `lowering_occurrences(total)` and `index_bytes(total)`. Overflow or any nonlatching check failure aborts. An allocation move transfers its reservation; copies coexist and both count. Release only when the represented ownership ends. Borrowed text costs zero copied bytes, but its container slots still count. Logical slots do not claim exact allocator capacity or heap/RSS metering.
|
||||
|
||||
Entry slots cover syntax/storage metadata, catalog/map/vector/work entries and expression sites. Occurrence slots cover lowerer occurrence records, using the existing entries-plus-reference ceiling. Each slot belongs to one domain. Owned text covers all separately owned in-scope String bytes, including copied keys, paths and retained diagnostic text. Original source bytes are borrowed and already cumulatively admitted; they are not recounted as copied auxiliary text. The auxiliary byte ceiling is distinct from cumulative source bytes.
|
||||
|
||||
| Representation / lifetime | Required reservation |
|
||||
| --- | --- |
|
||||
| Retained syntax, tokens, positions, file references and lexical ownership | Fixed all-file baseline: retain/reconstruct mixed metadata subtotal, plus parsed syntax-entry storage. Avoid counting tokens/positions twice. Existing dropped value-end-map subtotal may remain a conservative overestimate. Sum owned strings by a bounded walk of parsed structures, not a source rescan; counting scratch reserves before growth. Baseline stays live in accepted result. |
|
||||
| Complete catalog, ordinary alias projection, diagnostic location registry | Count every distinct map/set/container entry; reserve owned key copies before cloning. Borrow names where existing APIs permit. Projection drops after ordinary construction; catalog drops after lowering once retained locations no longer depend on it. |
|
||||
| Ordinary construction coordination | Borrowed ordered occurrence/file views and deferred-node vector reserve before growth. Nodes move into diagrams; coordination storage then drops. The active-reference hook below runs before field cloning. |
|
||||
| Typed lowering | Reserve lower occurrence/expression sites and Resolver work/temporary structures before growth across all files. Site vectors and their reservations remain in the accepted result; temporary work reservations end after use. Model maps move once. |
|
||||
| Diagnostic mapping and retention | Source mappings, output/retained diagnostic containers, related-location entries, transferred retained strings and newly copied strings share the still-live syntax/sites/locations reservations. Reserve before copy/insertion/retention. Compute line/Unicode column from borrowed file prefixes, or reserve any new cache explicitly. |
|
||||
|
||||
This is **auxiliary** accounting, not preallocation admission of every ordinary serde/model allocation. Existing architectural validation, its indexes and its produced raw `Vec<Issue>`/issue-owned strings are explicitly outside this preallocation ledger, as are the architecture/wire allocations excluded by the static meter. An optional cap checked after raw Issue production is only post-production checking. A raw Issue vector must not remain uncharged in the accepted witness: map/move its contents only after acquiring retained reservations, or drop it. Transferring a String avoids a copy but still requires its new retained reservation; copying requires an additional reservation before the copy. If mapped/retained storage cannot fit, return a resource failure, not a success with truncated warnings.
|
||||
|
||||
Retained caller max_records/max_graph_objects are enforced on complete Project before success. Deferred nested model checks are not described as preallocation checks. These exclusions must remain visible in code documentation and evidence; neither this ledger nor static events constitutes whole-resolution CPU/allocation metering.
|
||||
|
||||
## 4. Active ordinary reference reservation
|
||||
|
||||
Add a narrow private hook at the start of each ordinary declaration's construction iteration, **before** the existing `fields.iter().map(Ast::resolve).collect()` and any resulting JSON/model vectors. Borrow witnessed arrays selected by DeclKind and these exact existing body/tag paths:
|
||||
|
||||
| Declaration | Active ordered array |
|
||||
| --- | --- |
|
||||
| Process, FunctionalChain, Scenario element | top-level `steps` |
|
||||
| Occurrence element | `body` → externally tagged `Branch` object → `occurrences` |
|
||||
| ContextualLink relation | `source.references`, then `target.references` |
|
||||
|
||||
These selectors mirror `ParseLimits::check_model` and `ElementBody::steps`. In declaration/item order, charge each occurrence once through the retained `Budget.reference`, including repeated targets and UUID items as well as @references. Charge neither unique-target count nor separate per-file allowances. Locate refusal at the first unaffordable item in its original file; use stored extent or an explicit item-start/field fallback.
|
||||
|
||||
Absent/default-empty arrays cost zero. For malformed shapes, conservatively reserve recognizable active arrays and let existing conversion diagnose invalid shapes/items; do not add a decoder just for accounting. A private resource failure can precede malformed-item conversion. Existing public wrappers retain ordinary behavior through a no-op hook.
|
||||
|
||||
Exclude scalar parent/owner, endpoint occurrence, purpose fields, unrelated arrays named references, already charged typed fields and **all historical Trace baseline snapshots**. Baseline/model storage still participates in final model limits. The counter therefore equals parsed typed references plus active ordinary steps/contextual-chain items. Final check_model validates the complete model without charging that counter again.
|
||||
|
||||
## 5. One admission route, complete warnings and locations
|
||||
|
||||
After checked merge and complete model limits, use a narrow shared internal validator route, shared by mixed resolution and the existing `validation::validate` wrapper. Run `admit_structure_checked` once. On success, extract owned static limits/usage, drop the Project-borrowing environment, then run `validate_structure` once and preserve its complete raw warning/error result for mapping. On static failure preserve existing short-circuit behavior. Do not additionally call `analysis::admit`, which would repeat static/architecture work. Existing standalone admit/validate public behavior must remain compatible; no cached/self-borrowing environment enters the new witness.
|
||||
|
||||
Static limits use the existing declared profile and must be recorded with usage; they cannot silently replace or reset the syntax/model policies. A shared internal result carries successful static metadata and complete issues to mapping. Architectural Issue production has the exclusion in §3; subsequent mapped/retained allocation does not.
|
||||
|
||||
Retain every warning on successful acceptance. Any error refuses the witness. Locations cover ordinary and typed globals plus calculation-scoped parameters/fields, using actual canonical file ownership and full-file offsets. Preserve typed failure file keys and ordinary occurrence mapping. An unmapped semantic field falls back to its owning declaration explicitly. Never choose a file by coincident local byte offset, globalize ParameterId or fabricate a precise field span.
|
||||
|
||||
## 6. Discriminating acceptance oracles
|
||||
|
||||
All are requirements for authored fixtures and execution, not reported results. Expected models, spans, warning content and thresholds must be independently stated; formatting/lowering the tested input cannot generate its own oracle.
|
||||
|
||||
1. **Complete bidirectional model:** tiny OA and SA bundles with ordinary node→later typed property and typed subject→later ordinary work element. Compare entire independently built Project, including ordinary defaults, diagrams and definitions. Reordered presentation retains intended semantics and original source ownership.
|
||||
2. **Catalog and legacy boundaries:** interleaved ordinary/typed duplicate aliases and IDs, including simultaneous collision and entry-project collision, prove alias-first errors with both locations before insertion. Scoped parameter UUID/global reuse succeeds. Separately preserve source3 scratch migration, current required-field errors, lexical-parent conflict and omitted Scenario viewpoint behavior through unchanged public source3–7 wrappers.
|
||||
3. **Full retained-site oracle:** repeated calls with reversed argument-label order, two calculations sharing parameter UUID/name, nested child expressions and coincident local spans in distinct files. Hand-enumerate every `(file, owner, field, parent-edge/path, role, qualified target, original span)`. Resolve private arena positions to these authored structural identities; check every occurrence.expression targets the intended entry. Do not invoke Index or lower again. Include both directions of ordinary/typed references in whole-Project comparison. Model equality alone cannot satisfy this test.
|
||||
4. **Inherited and aggregate limits:** hand-count active ordinary lists/chains plus typed references; exact cap succeeds, cap-minus-one rejects before field cloning with the correct item/file. Repeat ordered occurrences and positive parent/baseline exclusions. Two simultaneously live catalog/projection producers each fit alone but exceed aggregate storage together. Different retained nested-record caps distinguish final admission on identical syntax without replacement limits. Parse/Exact counters remain unchanged; reference delta equals the independent active count.
|
||||
5. **Admission/diagnostic producer:** one exact warning-only complete model preserves that warning; one static failure and one architecture failure return no witness. A tight mapped-diagnostic storage cap fails while retained syntax/sites remain live, including copied path/text or transferred-string reservation. Instrument the narrow internal route or inspect its control flow to prove one static admission and one architecture pass on success; equal outputs alone do not prove this. Do not call raw Issue production preallocation-bounded.
|
||||
6. **Provenance and cleanup:** complete entry/file/path ownership, Unicode/CRLF coordinates, unchanged global arena/file keys, no externally replaceable authorities and no partially usable failed result. Run relevant existing typed-only lower/export, public parser/traversal and small-stack success/rejection/drop controls after shared-code changes. No source8/editing success is inferred from these controls.
|
||||
|
||||
## 7. Deferred dependency and scope boundary
|
||||
|
||||
Next typed-Index work must account for names, owner/physical/path/call maps and syntax-edge verification tables against the retained ledger, use original SourceBundle/tokens through a private read-only view, and refuse/hide that view from the old numeric-map updater. It must derive names once from complete Project plus scoped parameters, rather than reusing both complete Project and former Lowered model maps. Ordinary occurrences are still not provided by a typed Index.
|
||||
|
||||
Public7→8 dispatch/migrations/admission, missing-ID stabilization, ordinary+typed composition against original bytes, output recompile/admission and intended-Project equality, history/checked-save, root Scenario/editor reconciliation and full graphical/textual parity remain coordinated later work. This checkpoint does not claim those capabilities or a complete occurrence index. Parent owns final scope acceptance, decision/publication and gate disposition.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user