Skip to content

Commit 927b260

Browse files
committed
补齐 JavaScript 独立快检并约束自动原生工具范围
1 parent ead1514 commit 927b260

20 files changed

Lines changed: 1416 additions & 17 deletions

‎README.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -819,4 +819,6 @@ Native-first and WASM-first Rust syntax tasks now share a protected-host SDK con
819819

820820
See [WASM-first acceptance](tests/acceptance/rust-wasm-task-resolution.md) for the actual execution path, protocols and counterexample routing.
821821

822-
JavaScript fallback candidates now reach project checks and confirmed edit hooks: duplicate direct simple let/const bindings retain separate structural evidence, reuse one ESLint confirmation task, and produce native-check guidance through `next`. This is bounded candidate support; standalone lint integration, actual host acceptance and grammar qualification remain pending. See [acceptance](tests/acceptance/javascript-binding-workbench.md).
822+
JavaScript fallback candidates now reach project checks and confirmed edit hooks: duplicate direct simple let/const bindings retain separate structural evidence, reuse one ESLint confirmation task, and produce native-check guidance through `next`. The standalone `lint typescript` entry also selects the JavaScript grammar for `.js`, `.mjs`, `.cjs` and `.jsx`, with native ESLint first. Candidate-free observations recommend native lint only when no prior confirmation task remains. Actual host acceptance and grammar qualification remain pending. See [acceptance](tests/acceptance/javascript-binding-workbench.md).
823+
824+
These additions describe the current source candidate; the public npm release is unchanged.

‎README.zh-CN.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -802,4 +802,6 @@ Rust 原生首次与 WASM 首次语法任务均可通过同一受保护宿主 SD
802802

803803
WASM 首次闭环的实际执行路径、协议与误报分流见 [验收记录](tests/acceptance/rust-wasm-task-resolution.md)。
804804

805-
JavaScript 回退候选已接入项目检查与确认编辑 Hook:重复顶层简单 let/const 绑定保留独立结构证据,沿用同一 ESLint 确认任务,并由 `next` 指导原生检查。这是有界候选能力;独立 lint 接线、真实宿主验收与 grammar 资格仍未完成。见[验收](tests/acceptance/javascript-binding-workbench.md)。
805+
JavaScript 回退候选已接入项目检查与确认编辑 Hook:重复顶层简单 let/const 绑定保留独立结构证据,沿用同一 ESLint 确认任务,并由 `next` 指导原生检查。独立 `lint typescript` 入口也已按 `.js`、`.mjs`、`.cjs`、`.jsx` 选择 JavaScript grammar,并优先调用原生 ESLint;只有无历史待确认任务的零候选观察才推荐安装 lint。真实宿主验收与 grammar 资格仍未完成。见[验收](tests/acceptance/javascript-binding-workbench.md)。
806+
807+
以上为当前源码候选的新增能力,公开 npm 发行未包含本批修改。

‎crates/codeguard-cli/src/eslint_lint_arguments.rs‎

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,11 @@ impl EslintLintArguments {
3535
while index < args.len() {
3636
let current = &args[index];
3737
if current.starts_with('-') {
38+
let (option, inline) = current
39+
.split_once('=')
40+
.map_or((current.as_str(), None), |(key, value)| (key, Some(value)));
3841
if !matches!(
39-
current.as_str(),
42+
option,
4043
"--node-tool"
4144
| "--eslint-entry"
4245
| "--config"
@@ -46,18 +49,24 @@ impl EslintLintArguments {
4649
| "--eslint-version"
4750
| "--format"
4851
| "--timeout"
49-
) || !seen.insert(current.clone())
52+
) || !seen.insert(option.to_owned())
5053
{
5154
return Err("ESLint 参数未知或重复".into());
5255
}
53-
index += 1;
54-
let value = args
55-
.get(index)
56-
.filter(|value| !value.starts_with('-'))
57-
.ok_or("ESLint 参数缺少值")?;
58-
match current.as_str() {
56+
let value = if let Some(value) = inline {
57+
value
58+
} else {
59+
index += 1;
60+
args.get(index)
61+
.map(String::as_str)
62+
.ok_or("ESLint 参数缺少值")?
63+
};
64+
if value.is_empty() || value.starts_with('-') {
65+
return Err("ESLint 参数缺少值".into());
66+
}
67+
match option {
5968
"--format" => {
60-
if !matches!(value.as_str(), "human" | "json") {
69+
if !matches!(value, "human" | "json") {
6170
return Err("ESLint 格式未知".into());
6271
}
6372
result.json = value == "json";
@@ -67,14 +76,14 @@ impl EslintLintArguments {
6776
if !codeguard_adapters::eslint_report_version_matches(value, value) {
6877
return Err("需要具体稳定 ESLint 10 版本".into());
6978
}
70-
result.version = Some(value.clone());
79+
result.version = Some(value.to_owned());
7180
}
7281
_ => {
7382
let path = PathBuf::from(value);
7483
if !path.is_absolute() || value.chars().any(char::is_control) {
7584
return Err("工具及原配置需要安全绝对路径".into());
7685
}
77-
match current.as_str() {
86+
match option {
7887
"--node-tool" => result.node = Some(path),
7988
"--eslint-entry" => result.entry = Some(path),
8089
"--config-map" => result.config_map = Some(path),
@@ -96,3 +105,22 @@ impl EslintLintArguments {
96105
Ok(result)
97106
}
98107
}
108+
109+
#[cfg(test)]
110+
mod tests {
111+
use super::EslintLintArguments;
112+
#[test]
113+
fn joined_and_separate_options_share_identity_and_reject_duplicates() {
114+
let parse = |args: &[&str]| {
115+
EslintLintArguments::parse(&args.iter().map(|v| (*v).to_owned()).collect::<Vec<_>>())
116+
};
117+
let separated = parse(&["input.js", "--format", "json", "--timeout", "2s"]).unwrap();
118+
let joined = parse(&["input.js", "--format=json", "--timeout=2s"]).unwrap();
119+
assert_eq!(joined.json, separated.json);
120+
assert_eq!(joined.timeout_ms, separated.timeout_ms);
121+
assert!(parse(&["input.js", "--format=json", "--format", "human"]).is_err());
122+
assert!(parse(&["input.js", "--format=", "--timeout=2s"]).is_err());
123+
assert!(parse(&["input.js", "--workspace="]).is_err());
124+
assert!(parse(&["input.js", "--unknown=json"]).is_err());
125+
}
126+
}

‎crates/codeguard-cli/src/eslint_lint_command.rs‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,12 @@ pub fn run(args: &[String]) -> ExitCode {
4343
}
4444
#[cfg(feature = "wasm-precheck")]
4545
if crate::typescript_syntax_precheck::eligible(&args) {
46-
let report = match crate::eslint_native_first_candidate::observed_candidate(&args.source) {
46+
let candidate = if let Some(workspace) = &args.workspace {
47+
crate::eslint_native_first_candidate::observed_candidate_within(&args.source, workspace)
48+
} else {
49+
crate::eslint_native_first_candidate::observed_candidate(&args.source)
50+
};
51+
let report = match candidate {
4752
Ok(Some(candidate)) => {
4853
let mut native_args = args.clone();
4954
native_args.entry = Some(candidate.entry);
@@ -61,6 +66,9 @@ pub fn run(args: &[String]) -> ExitCode {
6166
)
6267
}
6368
}
69+
Ok(None) if crate::javascript_syntax_precheck::supported(&args) => {
70+
crate::javascript_syntax_precheck::observe(&args, deadline)
71+
}
6472
Ok(None) => {
6573
let mut report = crate::typescript_syntax_precheck::observe(&args, deadline);
6674
connect_syntax_confirmation(&args, &mut report);
@@ -194,6 +202,10 @@ pub(crate) fn print_feedback(report: &Value) {
194202
);
195203
}
196204
}
205+
#[cfg(feature = "wasm-precheck")]
206+
if let Some(syntax) = report.get("syntax_candidates") {
207+
crate::javascript_syntax_precheck::print_feedback(syntax);
208+
}
197209
println!("工作台:{}", report["workbench_status"]);
198210
println!("下一步:{}", report["next_action"]);
199211
}
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
//! JavaScript 独立入口复用项目候选扫描与稳定任务;不替代原生 ESLint。
2+
use crate::eslint_lint_arguments::EslintLintArguments;
3+
use serde_json::{Value, json};
4+
use std::{collections::BTreeSet, time::Instant};
5+
6+
/// 判断请求是否属于 JavaScript grammar 的整文件输入;不推断 module/CommonJS 语义。
7+
pub(crate) fn supported(args: &EslintLintArguments) -> bool {
8+
args.source.extension().is_some_and(|extension| {
9+
["js", "mjs", "cjs", "jsx"]
10+
.iter()
11+
.any(|value| extension == *value)
12+
})
13+
}
14+
15+
/// 在原生上下文发现为空后,以同一截止时间观察单文件并同步既有确认任务。
16+
/// 参数为原请求及截止时间,返回无交付权威的版本化候选反馈。
17+
pub(crate) fn observe(args: &EslintLintArguments, deadline: Instant) -> Value {
18+
let mut report = json!({"schema_version":"0.6.0","report_type":"eslint_local_feedback",
19+
"status":"incomplete","local_coherent":false,"coverage_proven":false,
20+
"delivery_decision":"not_evaluated","reason":"eslint_execution_context_missing",
21+
"findings":[],"suppressed_count":0,"workbench_status":"not_connected","workbench":null,
22+
"native":{"status":"not_run","reason":"explicit_context_missing"},
23+
"setup":{"requirement":"required","reason":"native_confirmation_needed","task_id":null},
24+
"syntax_candidates":{"status":"not_run","reason":"source_scope_unavailable","observations":[]},
25+
"syntax_tasks":null,
26+
"next_action":"源码范围无法绑定;先核对源码位置及 --workspace 的已核验绝对路径,本次语法初检未运行,不修改源码"});
27+
let source = match args.source.canonicalize() {
28+
Ok(source) => source,
29+
Err(_) => return report,
30+
};
31+
let root = match args.workspace.as_deref().or_else(|| source.parent()) {
32+
Some(path) => match path.canonicalize() {
33+
Ok(root)
34+
if args
35+
.workspace
36+
.as_ref()
37+
.is_none_or(|workspace| *workspace == root) =>
38+
{
39+
root
40+
}
41+
_ => return report,
42+
},
43+
None => return report,
44+
};
45+
let Some(relative) = source
46+
.strip_prefix(&root)
47+
.ok()
48+
.and_then(|path| path.to_str())
49+
else {
50+
return report;
51+
};
52+
let selected = BTreeSet::from([relative.to_owned()]);
53+
let empty = Value::Null;
54+
let coverage = crate::rust_native_syntax_coverage::RustNativeSyntaxCoverage::default();
55+
let syntax = crate::check_syntax_candidates::observe_selected(
56+
&root,
57+
&selected,
58+
crate::check_syntax_candidates::NativeCoverage {
59+
node_lint: &empty,
60+
python_lint: &empty,
61+
go_lint: &empty,
62+
erlang_lint: &empty,
63+
kotlin_lint: &empty,
64+
zig_lint: &empty,
65+
swift_lint: &empty,
66+
ruby_lint: &empty,
67+
rust_targets: &coverage,
68+
go_tool: None,
69+
},
70+
1,
71+
deadline,
72+
None,
73+
);
74+
report["next_action"] = json!(
75+
"查看本次候选或检查未完成原因,恢复适用的原生 JavaScript/ESLint 上下文确认;不凭候选直接修改源码或关闭任务"
76+
);
77+
// 只有已完成本次有界扫描且没有任何候选,才将安装建议降为推荐;语法能力仍未验收。
78+
let no_candidates = syntax["status"] == "observed_partial"
79+
&& syntax["skipped_count"] == 0
80+
&& syntax["unrouted_count"] == 0
81+
&& syntax["observations"].as_array().is_some_and(|rows| {
82+
rows.len() == 1
83+
&& rows[0]["status"] == "candidate_observed"
84+
&& rows[0]["reason"].is_null()
85+
&& rows[0]["recovery_count"] == 0
86+
&& rows[0]["structural_observation_count"]
87+
.as_u64()
88+
.unwrap_or(0)
89+
== 0
90+
});
91+
if no_candidates {
92+
report["setup"]["requirement"] = json!("recommended");
93+
report["setup"]["reason"] = json!("native_lint_recommended");
94+
report["next_action"] = json!(
95+
"本次有界候选初检未发现疑似问题;推荐配置并运行原生 ESLint,类型、依赖、安全及完整项目质量仍未检查"
96+
);
97+
}
98+
if codeguard_runtime::sigint_cancellation_requested() {
99+
report["reason"] = json!("request_cancelled");
100+
} else if args.workspace.is_some() {
101+
let tasks = crate::syntax_confirmation::persist(&root, &syntax, deadline);
102+
report["workbench_status"] = tasks["status"].clone();
103+
let mut workbench = json!({"status":tasks["status"]});
104+
let selected_task = tasks["tasks"][0]["task_id"]
105+
.as_str()
106+
.map(str::to_owned)
107+
.or_else(|| {
108+
if no_candidates {
109+
pending_confirmation(&root, relative)
110+
} else {
111+
None
112+
}
113+
});
114+
if let Some(id) = selected_task {
115+
report["setup"]["task_id"] = json!(id);
116+
workbench["task_id"] = json!(id);
117+
if no_candidates {
118+
// 当前初检没有候选,不代表历史原生确认已完成;保留待办及明确安装要求。
119+
report["setup"]["requirement"] = json!("required");
120+
report["setup"]["reason"] = json!("native_confirmation_still_pending");
121+
}
122+
if let Ok(brief) = crate::next_command::read_task_brief(&root, &id) {
123+
report["next_action"] = brief["step"].clone();
124+
}
125+
}
126+
if tasks["failures"]
127+
.as_array()
128+
.is_some_and(|failures| !failures.is_empty())
129+
{
130+
report["next_action"] = json!(if tasks["failures"].as_array().is_some_and(|failures| {
131+
failures
132+
.iter()
133+
.any(|failure| failure["reason"] == "workspace_not_initialized")
134+
}) {
135+
"工作区未初始化;先对原工作区执行 codeguard init <工作区> --apply,再按原 lint 命令复检。保留初检候选,不修改源码或虚构任务"
136+
} else {
137+
"本次候选任务同步未完成;查看 syntax_tasks.failures,核查 .codeguard/reports 的路径、权限及受管工作区身份,再按原命令复检,不自行创建关闭证据"
138+
});
139+
}
140+
report["workbench"] = workbench;
141+
report["syntax_tasks"] = tasks;
142+
}
143+
report["syntax_candidates"] = syntax;
144+
report
145+
}
146+
147+
// 仅从已初始化工作区及经过完整工作台校验的现存任务恢复引用,不制造新任务或关闭证据。
148+
fn pending_confirmation(root: &std::path::Path, relative: &str) -> Option<String> {
149+
let baseline = crate::workspace_refresh::read_workspace_baseline(root)
150+
.ok()
151+
.flatten()?;
152+
let fingerprint = crate::eslint_preparation::fingerprint(baseline.workspace_id()?, relative);
153+
let id = format!("CG-B-{}", &fingerprint[..32]);
154+
let brief = crate::next_command::read_task_brief(root, &id).ok()?;
155+
if brief["checker_id"] != "node.eslint.preparation"
156+
|| brief["kind"] != "blocker"
157+
|| brief["scope"] != relative
158+
{
159+
return None;
160+
}
161+
Some(id)
162+
}
163+
164+
/// 输出有界脱敏语法与结构位置;参数为本轮候选报告,不输出源码片段或标识符。
165+
pub(crate) fn print_feedback(syntax: &Value) {
166+
println!(
167+
"内置 JavaScript 候选初检:{};原生 lint 尚未完成。",
168+
syntax["status"]
169+
);
170+
for row in syntax["observations"]
171+
.as_array()
172+
.into_iter()
173+
.flatten()
174+
.take(1)
175+
{
176+
println!(
177+
"疑似恢复节点 {},结构候选 {};检查原因 {}。",
178+
row["recovery_count"],
179+
row["structural_observation_count"].as_u64().unwrap_or(0),
180+
row["reason"]
181+
);
182+
for recovery in row["recoveries"].as_array().into_iter().flatten().take(8) {
183+
println!(
184+
"疑似语法 {},零起始行 {}、UTF-8 字节列 {};须原生确认。",
185+
recovery["kind"], recovery["start_row"], recovery["start_column_byte"]
186+
);
187+
}
188+
for structure in row["structural_observations"]
189+
.as_array()
190+
.into_iter()
191+
.flatten()
192+
.take(8)
193+
{
194+
println!(
195+
"疑似结构规则 {},零起始行 {}、UTF-8 字节列 {};须原生确认。",
196+
structure["rule_id"], structure["start_row"], structure["start_column_byte"]
197+
);
198+
}
199+
}
200+
}

‎crates/codeguard-cli/src/lib.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,8 @@ pub mod tool_identity;
264264
pub mod tool_lock;
265265
#[cfg(feature = "wasm-precheck")]
266266
mod typescript_syntax_precheck;
267+
#[cfg(all(unix, feature = "wasm-precheck"))]
268+
mod javascript_syntax_precheck;
267269
pub mod whitelist_command;
268270
mod whitelist_correction_command;
269271
mod whitelist_correction_projection;

‎crates/codeguard-cli/src/typescript_syntax_precheck.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use sha2::{Digest, Sha256};
1010
use std::sync::atomic::AtomicBool;
1111
use std::time::Instant;
1212

13-
/// 仅为无任何显式原生上下文的 TypeScript 普通单文件请求启用候选初检。
13+
/// 仅为无任何显式原生上下文的 TypeScript/JavaScript 普通单文件请求启用候选初检。
1414
/// 参数为原命令请求;返回是否可尝试补充语法观察,不断言原生工具已缺失。
1515
pub(crate) fn eligible(args: &EslintLintArguments) -> bool {
1616
args.node.is_none()
@@ -21,7 +21,7 @@ pub(crate) fn eligible(args: &EslintLintArguments) -> bool {
2121
&& args.version.is_none()
2222
&& std::fs::symlink_metadata(&args.source).is_ok_and(|metadata| metadata.is_file())
2323
&& args.source.extension().is_some_and(|extension| {
24-
["ts", "tsx", "mts", "cts"]
24+
["ts", "tsx", "mts", "cts", "js", "jsx", "mjs", "cjs"]
2525
.iter()
2626
.any(|value| extension == *value)
2727
})

0 commit comments

Comments
 (0)