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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,8 @@ Generic `dependencies`, generic `security`, arbitrary category/language combinat

An opt-in source build with `--features codeguard-cli/wasm-precheck` has narrow TypeScript and Java single-file candidate paths. For TypeScript, a visible local ESLint 10 entry and one flat config take priority: when an executable Node is resolved from `PATH`, the CLI runs its existing bounded native probe; without Node it reports the setup gap. Only when the project-local ESLint package is not observed can the bundled TypeScript/TSX candidate provide suspected syntax positions; an untrusted entry, package identity or config returns a specific setup blocker. The Java candidate likewise remains incomplete; no bundled grammar has an accepted language/dialect range. Native observations and prechecks never approve delivery. Default and published binaries keep their previous native-context behavior. [TypeScript acceptance](tests/acceptance/typescript-syntax-fallback-candidate.md) · [native-first acceptance](tests/acceptance/native-first-eslint-candidate.md) · [Java acceptance](tests/acceptance/java-syntax-fallback-candidate.md).

With an initialized `--workspace`, that TypeScript/TSX fallback now returns a real, stable native-confirmation task ID after sync; repeated WASM scans never close it. The individual suspected positions and a capability-matched native closure still need implementation. [Workbench acceptance](tests/acceptance/typescript-syntax-confirmation-task.md).

```bash
codeguard lint java .
codeguard lint typescript .
Expand Down
2 changes: 2 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,8 @@ Python 编辑快反馈可执行 `codeguard lint python . --file src/changed.py -

可选源码构建已有 Java/TypeScript 单文件候选路径。TypeScript 项目若有可见的本地 ESLint 10 入口和唯一 flat config,会先尝试从 `PATH` 解析 Node 并运行既有有界原生探针;缺 Node 时报告准备缺口;仅本地 ESLint 包未观察到时才提供 TypeScript/TSX 疑似语法初检。入口、包身份或配置不可信时保留具体环境阻塞。Java 候选仍须原生确认,所有内置 grammar 的版本/方言尚未验收,原生局部观察和初检都不能批准交付。默认及公开二进制沿用既有原生上下文路径,见 [Java 局部验收](tests/acceptance/java-syntax-fallback-candidate.md)、[TypeScript 局部验收](tests/acceptance/typescript-syntax-fallback-candidate.md)与[原生优先局部验收](tests/acceptance/native-first-eslint-candidate.md)。

已初始化工作区使用 `--workspace` 时,TypeScript/TSX 候选回退在同步后返回真实且稳定的原生确认任务 ID;反复 WASM 扫描不会自行关闭任务。逐个疑似位置的持久关联和能力匹配的原生关闭仍待实现,见[局部验收](tests/acceptance/typescript-syntax-confirmation-task.md)。

```bash
codeguard lint java .
codeguard lint typescript .
Expand Down
34 changes: 33 additions & 1 deletion crates/codeguard-cli/src/eslint_lint_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,11 @@ pub fn run(args: &[String]) -> ExitCode {
)
}
}
Ok(None) => crate::typescript_syntax_precheck::observe(&args, deadline),
Ok(None) => {
let mut report = crate::typescript_syntax_precheck::observe(&args, deadline);
connect_syntax_confirmation(&args, &mut report);
report
}
Err(reason) => observe_with_preparation_reason(&args, deadline, Some(reason)),
};
if args.json {
Expand All @@ -87,6 +91,34 @@ pub fn run(args: &[String]) -> ExitCode {
3
})
}
#[cfg(feature = "wasm-precheck")]
fn connect_syntax_confirmation(args: &EslintLintArguments, report: &mut Value) {
let Some(workspace) = &args.workspace else {
return;
};
report["schema_version"] = json!("0.5.0");
let reason = if report["syntax_precheck"]["checked_files"] == 1
&& report["syntax_precheck"]["source_sha256"].is_string()
&& report["syntax_precheck"]["grammar_sha256"].is_string()
{
"eslint_syntax_confirmation_needed"
} else {
"eslint_syntax_precheck_unavailable"
};
report["workbench"] = match workspace.canonicalize() {
Ok(root) if root == *workspace => {
crate::eslint_workbench::connect_preparation(&root, args, reason)
Comment on lines +108 to +110

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Bind the confirmation task to the precheck snapshot

When the source is edited while the WASM worker is running, observe returns observations and source_sha256 for the old bytes, but this call lets eslint_preparation::prepare reread and hash the new bytes without comparing the two digests. Sync can therefore succeed and return a real task ID whose evidence refers to the new source while the same feedback displays suspected positions from the old source; pass the precheck digest/snapshot into preparation or reject the connection when the current digest differs.

Useful? React with 👍 / 👎.

}
_ => json!({"status":"workspace_invalid"}),
};
report["workbench_status"] = report["workbench"]["status"].clone();
if let Some(task_id) = report["workbench"]["task_id"].as_str() {
report["setup"]["task_id"] = json!(task_id);
if let Some(step) = report["workbench"]["next"]["repair_brief"]["step"].as_str() {
report["next_action"] = json!(step);
}
}
}
pub(crate) fn observe_with_preparation(args: &EslintLintArguments, deadline: Instant) -> Value {
observe_with_preparation_reason(args, deadline, None)
}
Expand Down
8 changes: 8 additions & 0 deletions crates/codeguard-cli/src/eslint_preparation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,8 @@ pub(crate) fn valid_reason(reason: &str) -> bool {
matches!(
reason,
"eslint_execution_context_missing"
| "eslint_syntax_confirmation_needed"
| "eslint_syntax_precheck_unavailable"
| "eslint_node_runtime_unresolved"
| "eslint_project_manifest_untrusted"
| "eslint_local_dependency_path_untrusted"
Expand Down Expand Up @@ -302,6 +304,12 @@ pub(crate) fn guidance(root: &Path, fact: &Value) -> Value {
return fallback;
}
let step = match report["diagnostic_reason"].as_str() {
Some("eslint_syntax_confirmation_needed") => {
"当前源码只有未验收 WASM 语法初检;核对本轮疑似位置,恢复项目适用的原生 TypeScript/ESLint 语法检查并复检同一输入。初检正常、安装完成或任务勾选均不能关闭确认任务"
}
Some("eslint_syntax_precheck_unavailable") => {
"本轮内置语法初检未完成;核对 CLI 的具体未完成原因,恢复项目适用的原生 TypeScript/ESLint 语法检查并复检同一输入,不按未完成报告修改源码或关闭任务"
}
Some("eslint_node_runtime_unresolved") => {
"项目本地 ESLint 候选与原配置已发现;核对并提供受控 Node 路径后执行原生复检,不重复安装 ESLint 或修改无依据源码"
}
Expand Down
22 changes: 21 additions & 1 deletion crates/codeguard-cli/src/eslint_workbench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,27 @@ pub(crate) fn connect(
}
pub(crate) fn connect_preparation(root: &Path, args: &EslintLintArguments, reason: &str) -> Value {
match crate::eslint_preparation::prepare(root, args, reason) {
Ok(report) => persist(root, report, "node.eslint.preparation"),
Ok(report) => {
let id = report["blocker_id"].as_str().unwrap_or("").to_owned();
let mut result = persist(root, report, "node.eslint.preparation");
if matches!(
reason,
"eslint_syntax_confirmation_needed" | "eslint_syntax_precheck_unavailable"
) && result["status"] == "synced_partial"
{
if let Ok(brief) = crate::next_command::read_task_brief(root, &id) {
result["task_id"] = json!(id);
result["next"] = json!({
"schema_version":"0.1.0","report_type":"repair_brief_preview",
"operation":"next","command_status":"complete","exit_code":0,
"disposition":brief["disposition"],"reason":"current_source_confirmation_task",
"repair_brief":brief,"next_actions":[],
"authority":"local_unverified","delivery_decision":"not_evaluated"
});
}
}
result
}
Err(reason) => json!({"status":reason}),
}
}
Expand Down
167 changes: 167 additions & 0 deletions crates/codeguard-cli/tests/typescript_syntax_fallback_candidate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,173 @@ fn missing_native_context_yields_scoped_suspected_observation_and_incomplete_gat
assert!(!project.0.join(".codeguard").exists());
}

#[test]
fn initialized_wasm_precheck_keeps_one_native_confirmation_task() {
let project = Project::new();
let root = project.0.canonicalize().unwrap();
let source = root.join("bad.ts");
fs::write(&source, "const x: number = ;\n").unwrap();
let init = Command::new(env!("CARGO_BIN_EXE_codeguard"))
.args([
"init",
root.to_str().unwrap(),
"--apply",
"--format",
"json",
])
.output()
.unwrap();
assert_eq!(init.status.code(), Some(3));
let lint = || {
let output = Command::new(env!("CARGO_BIN_EXE_codeguard"))
.args([
"lint",
"typescript",
source.to_str().unwrap(),
"--workspace",
root.to_str().unwrap(),
"--format",
"json",
])
.output()
.unwrap();
assert_eq!(output.status.code(), Some(3));
serde_json::from_slice::<Value>(&output.stdout).unwrap()
};
let first = lint();
assert_eq!(first["schema_version"], "0.5.0");
let schema: Value = serde_json::from_str(include_str!(
"../../../schemas/eslint-local-feedback-v0.5.schema.json"
))
.unwrap();
assert_eq!(schema["properties"]["schema_version"]["const"], "0.5.0");
assert_eq!(schema["additionalProperties"], false);
let actual: std::collections::BTreeSet<_> = first.as_object().unwrap().keys().collect();
let required: std::collections::BTreeSet<_> = schema["required"]
.as_array()
.unwrap()
.iter()
.map(|value| value.as_str().unwrap())
.collect();
assert_eq!(
actual
.iter()
.map(|value| value.as_str())
.collect::<std::collections::BTreeSet<_>>(),
required
);
assert!(
!first["syntax_precheck"]["observations"]
.as_array()
.unwrap()
.is_empty()
);
assert_eq!(first["workbench_status"], "synced_partial");
assert_eq!(first["workbench"]["new_blockers"], 1);
assert!(
first["next_action"]
.as_str()
.unwrap()
.contains("WASM 语法初检")
);
let task_id = first["setup"]["task_id"].as_str().unwrap();
assert!(
root.join(".codeguard/tasks")
.join(format!("{task_id}.md"))
.is_file()
);
let fact: Value = serde_json::from_slice(
&fs::read(
root.join(".codeguard/findings")
.join(task_id)
.join("finding.json"),
)
.unwrap(),
)
.unwrap();
assert_eq!(fact["kind"], "blocker");
assert_eq!(fact["checker_id"], "node.eslint.preparation");
assert_eq!(fact["state"], "open");
let second = lint();
assert_eq!(second["setup"]["task_id"], task_id);
assert_eq!(second["workbench"]["new_blockers"], 0);
assert_eq!(second["delivery_decision"], "not_evaluated");
fs::write(&source, "const x: number = 1;\n").unwrap();
let changed = lint();
assert_eq!(changed["setup"]["task_id"], task_id);
assert_eq!(changed["workbench"]["new_blockers"], 0);
assert_eq!(changed["delivery_decision"], "not_evaluated");

let tsx = root.join("Component.tsx");
fs::write(&tsx, "const App = () => <div>;\n").unwrap();
let tsx_output = Command::new(env!("CARGO_BIN_EXE_codeguard"))
.args([
"lint",
"typescript",
tsx.to_str().unwrap(),
"--workspace",
root.to_str().unwrap(),
"--format",
"json",
])
.output()
.unwrap();
assert_eq!(tsx_output.status.code(), Some(3));
let tsx_report: Value = serde_json::from_slice(&tsx_output.stdout).unwrap();
assert_eq!(tsx_report["schema_version"], "0.5.0");
assert_eq!(tsx_report["syntax_precheck"]["language"], "tsx");
assert_eq!(tsx_report["workbench"]["new_blockers"], 1);
assert_ne!(tsx_report["setup"]["task_id"], task_id);
assert_eq!(
tsx_report["workbench"]["next"]["repair_brief"]["task_id"],
tsx_report["setup"]["task_id"]
);
}

#[test]
fn initialized_workspace_keeps_precheck_failure_as_native_setup_task() {
let project = Project::new();
let root = project.0.canonicalize().unwrap();
let source = root.join("large.ts");
fs::write(&source, vec![b'a'; 1024 * 1024 + 1]).unwrap();
let init = Command::new(env!("CARGO_BIN_EXE_codeguard"))
.args([
"init",
root.to_str().unwrap(),
"--apply",
"--format",
"json",
])
.output()
.unwrap();
assert_eq!(init.status.code(), Some(3));
let output = Command::new(env!("CARGO_BIN_EXE_codeguard"))
.args([
"lint",
"typescript",
source.to_str().unwrap(),
"--workspace",
root.to_str().unwrap(),
"--format",
"json",
])
.output()
.unwrap();
assert_eq!(output.status.code(), Some(3));
let report: Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(report["schema_version"], "0.5.0");
assert_eq!(report["syntax_precheck"]["reason"], "source_unavailable");
assert_eq!(report["workbench_status"], "synced_partial");
assert!(report["setup"]["task_id"].as_str().is_some());
assert!(report["findings"].as_array().unwrap().is_empty());
assert!(
report["next_action"]
.as_str()
.unwrap()
.contains("初检未完成")
);
}

#[test]
fn no_recovery_remains_incomplete_and_javascript_does_not_use_typescript_grammar() {
let project = Project::new();
Expand Down
2 changes: 2 additions & 0 deletions docs/Codeguard-Command-Reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ Current slices include static discovery/init, selected native checks, partial ag

The opt-in `codeguard-cli/wasm-precheck` build adds narrow `lint typescript <file>` and `lint java <file>` paths when no explicit native execution context is provided. TypeScript first recognizes an ordinary project-local ESLint 10 entry and one flat config, then uses an executable Node from `PATH` to run the existing bounded native probe; when Node is unresolved, it reports setup instead of starting WASM. Only when no project-local ESLint package is observed does it return [TypeScript feedback 0.3.0](../schemas/eslint-local-feedback-v0.3.schema.json) or TSX 0.4.0 candidate feedback. Untrusted local paths, package identity or config selection remain setup blockers. Java returns [Java feedback 0.1.0](../schemas/java-syntax-precheck-feedback-v0.1.schema.json). These paths retain exit 3 and never turn an unvalidated grammar or local native observation into delivery approval. Default/published binaries retain the existing native-context behavior. See [native-first acceptance](../tests/acceptance/native-first-eslint-candidate.md).

With `--workspace` pointing to an initialized, canonical workspace, the TypeScript/TSX fallback emits [feedback 0.5.0](../schemas/eslint-local-feedback-v0.5.schema.json). It saves one source-scoped ESLint native-confirmation blocker and returns its real `setup.task_id` only after successful sync. Repeated scans and a later WASM result with no recovery nodes keep that task open; the individual suspected positions still need durable linkage and capability-matched native verification. See the [workbench acceptance record](../tests/acceptance/typescript-syntax-confirmation-task.md).

`codeguard hook plan [--format=json]` is a current, read-only host-event bridge. It reads a bounded [1.0 request](../schemas/hook-trigger-request.schema.json) from stdin and returns a [1.1 candidate tier](../schemas/hook-trigger-plan-v1.1.schema.json) with exit 3, `execution=not_run` and `delivery_decision=not_evaluated`. A confirmed edit may request fast file checks for at most eight distinct paths, 512 bytes per path and 2 KiB combined; an over-budget event instead requests scope resolution with `fast_scope_budget_exceeded`, without silently dropping files. This is a signal to select a bounded batch check, not to resubmit the same list forever. It does not invoke a checker, take a Git snapshot or block any host. The earlier [1.0 response schema](../schemas/hook-trigger-plan.schema.json) remains available for historical readers. See the [CLI acceptance record](../tests/acceptance/hook-plan-cli-candidate.md).

A separate Python execution slice now accepts `codeguard lint python . --file src/changed.py --format json`. Repeatable `--file` values share the eight-file, 512-byte per-path and 2-KiB combined budget. Only precisely selected, discovered Python files are scanned; an undiscovered target remains incomplete. The CLI conversation response is `python_lint_feedback` 0.13 with `scan_scope=selected_files` and `requested_paths`; it is not imported as a full workbench scan and keeps `delivery_decision=not_evaluated`. The previous 0.12 schema remains for historical results; the nested Python result in `check all` has not been upgraded. `hook plan` does not yet invoke this command automatically; other languages, hosts and bounded batch execution remain unwired.
Expand Down
2 changes: 2 additions & 0 deletions docs/Codeguard-Command-Reference.zh_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@

[已验收实现切片](../openspec/changes/introduce-rust-codeguard-cli/implementation-baseline.md)跟踪当前局部能力;[tasks](../openspec/changes/introduce-rust-codeguard-cli/tasks.md)独占完成状态。WASM 的完整统一入口仍为 S14 待办。可选 `codeguard-cli/wasm-precheck` 构建处理 `lint typescript <显式单文件>` 时,若观察到本地 ESLint 10 入口与唯一 flat config,会优先用 `PATH` 中可执行 Node 调用既有有界原生探针;缺 Node 则返回准备缺口。仅未观察到本地 ESLint 包时才输出 [TypeScript 0.3.0 候选反馈](../schemas/eslint-local-feedback-v0.3.schema.json)或 TSX 0.4.0。本地入口、包身份或配置不可信时保留具体环境阻塞,不启动 WASM。`lint java <显式单文件>` 仍输出 [Java 0.1.0 候选反馈](../schemas/java-syntax-precheck-feedback-v0.1.schema.json)。这些局部路径均不批准交付并保持退出 3;部分显式参数、Javadoc/Checkstyle 和符号链接沿用原路径,默认及已发布二进制也不含这项自动调度。见[原生优先局部验收](../tests/acceptance/native-first-eslint-candidate.md)与技术方案 5.3–5.4;后文未标当前的语法、退出0/1与闭环承诺均为目标契约。

对已初始化且使用规范绝对路径的 `--workspace`,TypeScript/TSX 候选回退改用[反馈 0.5.0](../schemas/eslint-local-feedback-v0.5.schema.json):按源码范围保存同一张 ESLint 原生确认阻塞任务,仅同步成功时返回真实 `setup.task_id`。重复扫描和后续 WASM 无恢复节点不关闭任务。逐个疑似位置的持久关联及能力匹配原生复检仍未接通,见[局部验收](../tests/acceptance/typescript-syntax-confirmation-task.md)。

## 1. 命令体系与协作路线

```mermaid
Expand Down
Loading
Loading