Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<Version>0.2.0-dev.19</Version>
<Version>0.2.0-dev.20</Version>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<LangVersion>12.0</LangVersion>
<Nullable>enable</Nullable>
Expand Down
9 changes: 9 additions & 0 deletions OpenVisionLab.MachineStudio.sln
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenVisionLab.MachineStudio
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{ABCDEF12-3456-7890-ABCD-EF0123456789}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenVisionLab.Machine.Persistence", "src\OpenVisionLab.Machine.Persistence\OpenVisionLab.Machine.Persistence.csproj", "{F05C6B7F-6D1C-419B-85D4-176F3BC6243D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -108,5 +110,12 @@ Global
{A4B5C6D7-E8F9-0123-4567-8901234DEF02}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A4B5C6D7-E8F9-0123-4567-8901234DEF02}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A4B5C6D7-E8F9-0123-4567-8901234DEF02}.Release|Any CPU.Build.0 = Release|Any CPU
{F05C6B7F-6D1C-419B-85D4-176F3BC6243D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F05C6B7F-6D1C-419B-85D4-176F3BC6243D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F05C6B7F-6D1C-419B-85D4-176F3BC6243D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F05C6B7F-6D1C-419B-85D4-176F3BC6243D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{F05C6B7F-6D1C-419B-85D4-176F3BC6243D} = {12345678-1234-1234-1234-123456789ABC}
EndGlobalSection
EndGlobal
13 changes: 6 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,19 @@ single-step, reset, dry-run, and layout playback workflows.

## Version

Current version: `v0.2.0-dev.19`
Current version: `v0.2.0-dev.20`

This project is maintained using explicit version numbers. The current branch
is a Release 2 development candidate; it is not a release tag or downloadable
package.

### Recent version history

#### `v0.2.0-dev.20` (2026-09-10)

- Organized the machine, simulation, sequence, vision, fault, and result code into responsibility folders while preserving namespaces and runtime contracts.
- Added headless shutdown termination coverage and kept the Release build plus 1,102 automated tests green.

#### `v0.2.0-dev.19` (2026-09-09)

- Preserved cancellation requests until the next bounded observation so a just-completed batch or commissioning validation is reported as `Cancelled`, not `Idle`.
Expand Down Expand Up @@ -66,12 +71,6 @@ package.
- Extracted wafer-handler runtime compilation into the WPF-neutral `MachineLayoutWaferHandlerRuntimeCompiler`.
- Preserved layout-device orchestration and verified focused, full Simulation, MachineStudio Sequence, and Release build gates.

#### `v0.2.0-dev.10` (2026-09-09)

- Extracted cross-sequence identity and cycle validation into the WPF-neutral `SequenceCompositionValidator`.
- Preserved the `SequenceCompiler` public compatibility entry point and verified Sequence, MachineStudio, full Simulation, and Release build gates.


## Download

Download the latest Windows x64 ZIP from
Expand Down
35 changes: 29 additions & 6 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,21 +71,44 @@ Camera acquisition and Vision evidence remain supported secondary capabilities.
They join a machine cycle when an inspection task requires them; they do not own
the core equipment-simulation workflow.

For current source navigation, read [DEVELOPER_ONBOARDING.md](DEVELOPER_ONBOARDING.md)
and [MVVM_ARCHITECTURE.md](MVVM_ARCHITECTURE.md). MachineStudio feature owners
live in shallow `ViewModel` domain folders; `MainViewModel.cs` remains the root
composition entrypoint. Folder movement preserves existing namespaces and does
not itself change the module dependency graph below.
## Source navigation

The shortest code-reading route follows the runtime rather than the UI tree:

```text
MachineStudio/MainViewModel
-> ViewModel/Simulation/Session/SimulationSessionCoordinator
-> Simulation/Engine/Runtime/FixedStepSimulationEngine
-> Simulation/Commands/Axis and Commands/Equipment (axis/I/O commands)
-> Sequence/Runtime and Simulation/Engine/Sequence
-> Vision contracts and Simulation/Scenarios/Vision
-> Simulation/Engine/Faults
-> Simulation/Scenarios/Evidence (result/evidence packages)
```

The physical folders make the ownership visible without changing namespaces or
public bindings. `ViewModel/Recipes` is grouped by authoring, dry-run, gallery,
semantic equipment, and station setup; `ViewModel/Simulation` is grouped by
faults, runtime, scenarios, session, and trace. Simulation commands are grouped
by axis, camera, equipment, faults, run control, and sequence. The corresponding
simulation tests use the same responsibility names, so a caller and its focused
test can be found by the same search term.

`MainViewModel.cs` remains the WPF composition entrypoint. Folder movement
preserves existing namespaces, XAML bindings, and the module dependency graph.

## Layered architecture

```text
MachineStudio (WPF composition and presentation)
-> Core, IO, Sequence, Simulation, Vision, Infrastructure
-> Core, IO, Persistence, Sequence, Simulation, Vision, Infrastructure

Infrastructure (project assets and external adapters)
-> Vision

Persistence (project JSON file adapter and catalog)
-> Core

Simulation (project compiler, fixed-step composition, runtime truth,
deterministic execution evidence)
-> Core, IO, Sequence
Expand Down
2 changes: 1 addition & 1 deletion docs/NET_VERSION_POLICY.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
- Language version: C# 12
- Nullable: enable
- ImplicitUsings: enable
- Product version: `0.2.0-dev.19`
- Product version: `0.2.0-dev.20`

The public repository is currently on the Release 2 development line. This is
not a release tag or a downloadable release; the release-candidate gates below
Expand Down
100 changes: 5 additions & 95 deletions src/OpenVisionLab.Machine.Core/Projects/ProjectDocumentStore.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.Runtime.ExceptionServices;
using System.Text.Json;
using System.Text.Json.Nodes;

Expand Down Expand Up @@ -75,73 +74,11 @@ public MachineProjectDocument Load(string json)
return doc;
}

public async Task SaveAsync(MachineProjectDocument document, string path, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(document);
ArgumentException.ThrowIfNullOrWhiteSpace(path);
var fullPath = Path.GetFullPath(path);
var directory = Path.GetDirectoryName(fullPath)
?? throw new ArgumentException("The project path must include a directory.", nameof(path));
var temporaryPath = Path.Combine(
directory,
$".{Path.GetFileName(fullPath)}.{Guid.NewGuid():N}.tmp");
var modifiedAt = DateTimeOffset.UtcNow;
var json = SerializeForSave(document, modifiedAt);

try
{
await File.WriteAllTextAsync(temporaryPath, json, cancellationToken).ConfigureAwait(false);
if (File.Exists(fullPath))
{
File.Replace(temporaryPath, fullPath, fullPath + ".bak", ignoreMetadataErrors: true);
}
else
{
File.Move(temporaryPath, fullPath);
}

ApplySaveMetadata(document, modifiedAt);
}
finally
{
if (File.Exists(temporaryPath))
{
File.Delete(temporaryPath);
}
}
}

public async Task<MachineProjectDocument> LoadAsync(string path, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(path);
var fullPath = Path.GetFullPath(path);
try
{
return await LoadFileAsync(fullPath, cancellationToken).ConfigureAwait(false);
}
catch (Exception primaryException) when (ShouldTryBackup(primaryException))
{
try
{
return await LoadFileAsync(fullPath + ".bak", cancellationToken).ConfigureAwait(false);
}
catch (Exception backupException) when (IsExpectedLoadFailure(backupException))
{
if (backupException is ProjectDocumentLoadException
{
ErrorCode: ProjectDocumentLoadErrorCode.UnsupportedSchema
})
{
ExceptionDispatchInfo.Capture(backupException).Throw();
}

ExceptionDispatchInfo.Capture(primaryException).Throw();
throw;
}
}
}

private string SerializeForSave(MachineProjectDocument document, DateTimeOffset modifiedAt)
/// <summary>
/// Creates the canonical on-disk JSON payload without mutating the document.
/// The file adapter applies the returned timestamp only after its atomic commit succeeds.
/// </summary>
public string SerializeForSave(MachineProjectDocument document, DateTimeOffset modifiedAt)
{
var root = JsonNode.Parse(Serialize(document))?.AsObject()
?? throw new InvalidOperationException("Failed to serialize project document.");
Expand All @@ -155,31 +92,4 @@ private static void ApplySaveMetadata(MachineProjectDocument document, DateTimeO
document.Schema = MachineProjectDocument.CurrentSchema;
document.ModifiedAt = modifiedAt;
}

private async Task<MachineProjectDocument> LoadFileAsync(
string path,
CancellationToken cancellationToken)
{
var json = await File.ReadAllTextAsync(path, cancellationToken).ConfigureAwait(false);
return Load(json);
}

private static bool ShouldTryBackup(Exception exception) => exception switch
{
ProjectDocumentLoadException
{
ErrorCode: ProjectDocumentLoadErrorCode.UnsupportedSchema
} => false,
_ => IsExpectedLoadFailure(exception)
};

private static bool IsExpectedLoadFailure(Exception exception) => exception switch
{
ProjectDocumentLoadException => true,
JsonException => true,
IOException => true,
UnauthorizedAccessException => true,
ArgumentOutOfRangeException => true,
_ => false
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using OpenVisionLab.Integration.Contracts;

namespace OpenVisionLab.Machine.Infrastructure.Integration;

public sealed record MachineIntegrationTransactionSummary(
IntegrationHandoffV2 Handoff,
bool HasAcknowledgement,
bool HasResult);

/// <summary>
/// Public compatibility facade for the Machine Studio side of the v2 file exchange.
/// Each operation delegates to the concrete owner for publication, inspection, or
/// retention. The facade contains no transaction policy or file-system workflow.
/// </summary>
public static class MachineIntegrationExchange
{
public const long DefaultMinimumFreeSpaceBytes = 1_048_576;

public static IntegrationHandoffV2 PublishHandoff(
string exchangeRoot,
IntegrationHandoffV2 handoff,
IReadOnlyDictionary<string, string> artifactSourcePaths) =>
MachineIntegrationTransactionPublisher.PublishHandoff(
exchangeRoot,
handoff,
artifactSourcePaths);

public static Task<IntegrationHandoffV2> PublishHandoffAsync(
string exchangeRoot,
IntegrationHandoffV2 handoff,
IReadOnlyDictionary<string, string> artifactSourcePaths,
IProgress<MachineIntegrationTransferProgress>? progress = null,
long minimumFreeSpaceBytes = DefaultMinimumFreeSpaceBytes,
CancellationToken cancellationToken = default) =>
MachineIntegrationTransactionPublisher.PublishHandoffAsync(
exchangeRoot,
handoff,
artifactSourcePaths,
progress,
minimumFreeSpaceBytes,
cancellationToken);

public static IReadOnlyList<MachineIntegrationTransactionSummary> DiscoverTransactions(
string exchangeRoot) =>
MachineIntegrationTransactionInspector.DiscoverTransactions(exchangeRoot);

public static IntegrationHandoffV2 ReadHandoff(
string exchangeRoot,
Guid transactionId) =>
MachineIntegrationTransactionInspector.ReadHandoff(exchangeRoot, transactionId);

public static IntegrationHandoffV2 ReadHandoffEnvelope(
string exchangeRoot,
Guid transactionId) =>
MachineIntegrationTransactionInspector.ReadHandoffEnvelope(exchangeRoot, transactionId);

public static IntegrationAcknowledgementV2 ReadAcknowledgement(
string exchangeRoot,
Guid transactionId) =>
MachineIntegrationTransactionInspector.ReadAcknowledgement(exchangeRoot, transactionId);

public static IntegrationResultV2 ReadResult(
string exchangeRoot,
Guid transactionId) =>
MachineIntegrationTransactionInspector.ReadResult(exchangeRoot, transactionId);

public static IReadOnlyList<MachineIntegrationTransactionDiagnostic> DiagnoseTransactions(
string exchangeRoot) =>
MachineIntegrationTransactionInspector.DiagnoseTransactions(exchangeRoot);

public static MachineIntegrationCleanupReport CleanupStaging(
string exchangeRoot,
TimeSpan staleAfter,
DateTimeOffset? nowUtc = null,
CancellationToken cancellationToken = default) =>
MachineIntegrationTransactionRetention.CleanupStaging(
exchangeRoot,
staleAfter,
nowUtc,
cancellationToken);

public static int PurgeQuarantine(
string exchangeRoot,
TimeSpan retention,
DateTimeOffset? nowUtc = null,
CancellationToken cancellationToken = default) =>
MachineIntegrationTransactionRetention.PurgeQuarantine(
exchangeRoot,
retention,
nowUtc,
cancellationToken);
}
Loading