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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ repository = "https://github.com/wrightkit/wright"
# parser, emitter, detection, validation, and Workshop IR. This is the single
# released reference for the cutover — workspace crates consume it via
# `workshop-rs.workspace = true`.
workshop-rs = "=0.1.1"
workshop-rs = "=0.1.2"

[workspace.lints.rust]
# Unsafe operations inside an unsafe function still need an explicit block so
Expand Down
83 changes: 82 additions & 1 deletion crates/wright-driver/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use wright_analyzer::registry::LintConfig;
use wright_analyzer::service::{Origin as ServiceOrigin, Request, SemanticService};

use crate::config::{SessionConfig, SourceKind};
use crate::diag::{Diagnostic, Origin, Position, SourceSpan, Stage};
use crate::diag::{Diagnostic, Origin, Position, SourceSpan, Stage, span_from_ir};
use crate::input::{self, ResolvedInput};
use crate::result::{
AnalyzeResult, CheckResult, CompileResult, CompiledOutput, ConvertResult, ConvertTarget,
Expand Down Expand Up @@ -412,6 +412,7 @@ impl CompilerSession {
},
);
}
self.attach_workshop_completeness(&loaded);
self.attach_analysis(&loaded);
self.finish(command, CheckResult { ostw: None })
}
Expand All @@ -431,6 +432,7 @@ impl CompilerSession {
// then run the shared semantic service over the lowered program.
self.push_ostw_diagnostics(&loaded);
}
self.attach_workshop_completeness(&loaded);
let service = match self.service(&loaded) {
Ok(service) => service,
Err(diagnostic) => {
Expand Down Expand Up @@ -517,6 +519,7 @@ impl CompilerSession {
// then run the shared semantic service over the lowered program.
self.push_ostw_diagnostics(&loaded);
}
self.attach_workshop_completeness(&loaded);
let service = match self.service_with(&loaded, self.config.lint.clone()) {
Ok(service) => service,
Err(diagnostic) => {
Expand Down Expand Up @@ -698,6 +701,67 @@ impl CompilerSession {
}
}

/// Structural validation permits source-preserving Workshop fallbacks.
/// Surface those nodes as blocking semantic diagnostics before presenting
/// check/lint output as definitive. The catalog remains owned by
/// workshop-rs; this is only the consumer-side diagnostic projection.
fn attach_workshop_completeness(&mut self, loaded: &Loaded) {
if loaded.input.kind != SourceKind::Workshop {
return;
}
let mut issues = Vec::new();
if let Some(settings) = &loaded.program.settings {
collect_raw_settings(&settings.children, &mut issues);
}
for action in loaded.program.actions.iter() {
if let wir::Action::Call { name, span, .. } = action {
if name == "rawWorkshopAction"
|| self
.catalog
.entry(wright_workshop::catalog::Kind::Action, name)
.is_none()
{
issues.push((
if name == "rawWorkshopAction" {
"opaque-action"
} else {
"unknown-action"
},
name.clone(),
*span,
));
}
}
}
for value in loaded.program.values.iter() {
if let wir::Value::Call { name, .. } = &value.value {
if self
.catalog
.entry(wright_workshop::catalog::Kind::Value, name)
.is_none()
&& self
.catalog
.entry(wright_workshop::catalog::Kind::Operator, name)
.is_none()
{
issues.push(("unknown-value", name.clone(), value.span));
}
}
}
for (kind, name, span) in issues {
self.diagnostics.push(Diagnostic {
code: "workshop-semantic-incomplete".to_string(),
stage: Stage::Analysis,
severity: crate::diag::Severity::Error,
message: format!(
"raw Workshop construct '{name}' is preserved or unknown ({kind}); analysis and lint findings are not definitive"
),
span: span_from_ir(span, &loaded.program.files),
source: Some(loaded.origin.clone()),
});
}
}

fn finish<T: serde::Serialize>(&mut self, command: &str, result: T) -> Envelope<T> {
let diagnostics = std::mem::take(&mut self.diagnostics);
let has_error = diagnostics
Expand Down Expand Up @@ -739,6 +803,23 @@ impl CompilerSession {
}
}

fn collect_raw_settings(
nodes: &[wright_workshop::settings::SettingsNode],
issues: &mut Vec<(&'static str, String, Option<workshop_rs::source::Span>)>,
) {
for node in nodes {
match node {
wright_workshop::settings::SettingsNode::Group { children, .. } => {
collect_raw_settings(children, issues)
}
wright_workshop::settings::SettingsNode::Raw { name, span, .. } => {
issues.push(("raw-setting", name.clone(), *span))
}
_ => {}
}
}
}

/// Extract the `result` payload of a semantic-service request as JSON.
fn service_response(service: &SemanticService<'_>, request: &Request) -> serde_json::Value {
match service.handle(request) {
Expand Down
8 changes: 8 additions & 0 deletions crates/wright-opy/src/reconstruct.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1110,6 +1110,14 @@ impl<'a> Emitter<'a> {
span,
);
}
ModifyOp::RemoveFromArrayByIndex => {
self.issue(
"unsupported-modify-op",
"Modify ... Remove From Array By Index is outside the reconstruction \
surface (the OPY surface has no indexed remove-from-array form)",
span,
);
}
ModifyOp::Add
| ModifyOp::Subtract
| ModifyOp::Multiply
Expand Down
9 changes: 7 additions & 2 deletions crates/wright-ostw/src/reconstruct.rs
Original file line number Diff line number Diff line change
Expand Up @@ -621,7 +621,9 @@ impl<'a> Classifier<'a> {
| ModifyOp::Divide
| ModifyOp::Modulo => {}
ModifyOp::AppendToArray => {} // representable as `receiver.append(value)`
ModifyOp::RaiseToPower | ModifyOp::RemoveFromArray => {
ModifyOp::RaiseToPower
| ModifyOp::RemoveFromArray
| ModifyOp::RemoveFromArrayByIndex => {
self.error(ReconstructError::at(
"reconstruct-unsupported-modify-op",
format!("modifyOp:{}", op.as_str()),
Expand Down Expand Up @@ -1209,7 +1211,10 @@ fn assign_op_spelling(op: ModifyOp) -> &'static str {
ModifyOp::Multiply => "*=",
ModifyOp::Divide => "/=",
ModifyOp::Modulo => "%=",
ModifyOp::AppendToArray | ModifyOp::RaiseToPower | ModifyOp::RemoveFromArray => {
ModifyOp::AppendToArray
| ModifyOp::RaiseToPower
| ModifyOp::RemoveFromArray
| ModifyOp::RemoveFromArrayByIndex => {
unreachable!("classified")
}
}
Expand Down
5 changes: 4 additions & 1 deletion crates/wright-workshop/tests/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -405,9 +405,12 @@ fn cross_domain_member_spelling_collisions_are_the_documented_inventory() {
(
"None".to_string(),
vec![
"FacingReeval".to_string(),
"ChaseTimeReeval".to_string(),
"ChaseRateReeval".to_string(),
"Invis".to_string()
"Invis".to_string(),
"ThrottleReeval".to_string(),
"EffectReeval".to_string()
]
),
(
Expand Down
32 changes: 32 additions & 0 deletions docs/workshop/raw-workshop-p0-evidence-v1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Raw Workshop P0 evidence (2026-08-21)

The five artifacts are pinned by the owning `workshop-rs` manifest:
[`raw-workshop-p0-v1.json`](https://github.com/wrightkit/workshop-rs/blob/main/docs/evidence/raw-workshop-p0-v1.json).
The full artifacts remain external because the manifest records unresolved or
non-asserted redistribution rights.

All five standalone `workshop-rs-cli parse --locale ...` runs completed with
exit 0 on the recorded artifact hashes. Wright was run from PR #190's exact
head after switching the dependency to published `workshop-rs 0.1.2` and
refreshing `Cargo.lock`.

| artifact | locale | check | lint | semantic-incomplete diagnostics | lint findings | rule path |
| --- | --- | --- | --- | ---: | ---: | --- |
| ai-pve-zh-CN | zh-CN | expected blocked diagnostic | expected blocked diagnostic | 2651 | 2 | 5 rules |
| bastion-en-US | en-US | expected blocked diagnostic | expected blocked diagnostic | 1562 | 18 | 5 rules |
| defend-the-castle-en-US | en-US | expected blocked diagnostic | expected blocked diagnostic | 2017 | 73 | 5 rules |
| illari-zh-CN | zh-CN | expected blocked diagnostic | expected blocked diagnostic | 398 | 4 | 5 rules |
| overwatch-rework-en-US | en-US | expected blocked diagnostic | expected blocked diagnostic | 153 | 4 | 5 rules |

The `workshop-semantic-incomplete` diagnostics are intentional: they identify
raw settings, unknown catalog calls, and `rawWorkshopAction` preservation with
source spans, and make the envelope non-OK so findings cannot be presented as
definitive. Lint still executes its five registered rules, which is recorded
separately from the blocked semantic-confidence result.

Finding review classification for this rerun is `uncertain` for every finding
until the corresponding source construct is semantically understood; no
finding is accepted as a high-confidence default result merely because the
parser or structural WIR validation succeeded. The observed rule families were
`duplicate-condition`, `repeated-value`, `while-without-wait`, and
`min-wait-loop`.
Loading