Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
5a30132
fix: harden terminal-escape rendering across all MdsError variants (C…
dean0x Jul 24, 2026
d490dda
fix(esc-injection): Python belt-and-suspenders + non-vacuous E12 test…
dean0x Jul 24, 2026
b453771
refactor: fix misattributed doc comments and remove source_len slop i…
dean0x Jul 24, 2026
c30a78d
test(lint/diagnostic): delete canonical_json_raw_message_preserved [#…
dean0x Jul 24, 2026
f77a499
test(napi,wasm): strengthen T-13b/F6 with non-vacuous lintVirtual ESC…
dean0x Jul 24, 2026
25e5e25
docs(test): fix stale T-13b preamble comment in napi spec [#176]
dean0x Jul 24, 2026
ed24492
fix(lint/diagnostic): Cow sanitizer, file-key + warnings sanitization…
dean0x Jul 25, 2026
dfb21a5
fix(python): typed-path sanitization parity [#176]
dean0x Jul 25, 2026
555095f
test(napi,wasm,js): strict cross-surface ESC assertions [#176]
dean0x Jul 25, 2026
5d35da0
fix(cli/render): sanitize inputs not rendered frame — PF-014 [#176]
dean0x Jul 25, 2026
9ef6576
docs(lint/diagnostic): true boundary enumeration + PF-014 model [#176]
dean0x Jul 25, 2026
a2a9cb0
test(cli): de-vacuum T-9, fail-closed gates, watch coverage [#176]
dean0x Jul 25, 2026
9b1e3c3
fix(cli): unify sanitization on choke-point helpers [#176]
dean0x Jul 25, 2026
ae0fd23
docs: CHANGELOG + spec §7.5 sanitization invariant [#176]
dean0x Jul 25, 2026
7922e9b
refactor: simplify resolve-round seams [#176]
dean0x Jul 25, 2026
d5c7975
fix(core): widen escape class + wire-mode newline escaping [#176]
dean0x Jul 25, 2026
3c383ad
test: cross-surface coverage for widened class + wire newline [#176]
dean0x Jul 25, 2026
7f26e3f
docs: spec 7.5 escape class + one-way contract, CHANGELOG [#176]
dean0x Jul 25, 2026
5fa54f4
fix(cli): TTY-gate --diff/--check preview source neutralization [#176]
dean0x Jul 25, 2026
4f591c7
refactor(cli): consolidate render_unified_diff + colorize_unified_dif…
dean0x Jul 25, 2026
ad2a673
test(wasm): pin wire-mode newline escape on the WASM surface [#176]
dean0x Jul 25, 2026
61120e7
docs: strip raw control bytes from comments, correct boundary claims …
dean0x Jul 25, 2026
ba5dde0
fix(cli): escape error message/help at the stderr choke-point [#176]
dean0x Jul 25, 2026
f2e2874
docs: close the boundary-table gap, retarget display_sanitized [#176]
dean0x Jul 25, 2026
cb6d860
fix(cli): route all warning prints through eprint_warning [#176]
dean0x Jul 25, 2026
2db1e79
docs: record escalation decisions in review ledger [#176]
dean0x Jul 25, 2026
46fb326
fix(core): close U+061C, filename-forgery and rejection-reason gaps […
dean0x Jul 26, 2026
e145e41
fix(cli): route the sixth warning print, forward the sanitized cause …
dean0x Jul 26, 2026
e1d1d73
docs: narrow the #176 claims to what is delivered [#176]
dean0x Jul 26, 2026
35195d4
docs: state the boundary table as an audit list, not a closure proof …
dean0x Jul 26, 2026
2dc9828
fix(core): WIRE-escape untrusted identifiers in warning producers [#176]
dean0x Jul 26, 2026
70baafa
fix(cli): WIRE-escape every value interpolated into a terminal line […
dean0x Jul 26, 2026
b8364d9
test(cli): CI-enforced print discipline + e2e forgery vectors [#176]
dean0x Jul 26, 2026
f5830ef
docs: re-ratify the per-field escape rule, correct the claims [#176]
dean0x Jul 26, 2026
0e79385
test(cli): close the hoisted-format! and bare-argument guard bypasses…
dean0x Jul 26, 2026
c973331
docs: narrow the WIRE claim to what holds, disclose the mds-core resi…
dean0x Jul 26, 2026
8223adb
docs: record round-3 outcome in the review ledger [#176]
dean0x Jul 26, 2026
746ad02
docs(knowledge): update mds-lint feature knowledge base
dean0x Jul 26, 2026
0ce1966
test(cli): close the non-let binder bypass, pin the core precondition…
dean0x Jul 26, 2026
731c3b3
docs: carve source maps out of the per-field escape rule [#176]
dean0x Jul 26, 2026
25e1533
docs(review): record round 4 in the #176 ledger [#176]
dean0x Jul 26, 2026
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

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions .devflow/features/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Feature Knowledge Index

- **mds-fmt** — crates/mds-core/src, crates/mds-cli/src — Use when modifying the mds fmt formatter engine (crates/mds-core/src/formatter.rs), the mds fmt CLI subcommand (crates/mds-cli/src/fmt.rs), any change to mds-core's output model (clean_output, evaluate_nodes, @message/@define body evaluation, the lexer's fence recognition) that could silently break the formatter's compile-equivalence guarantee, or changes to the shared directory walker (output.rs). Keywords: mds fmt, format_str, format_str_with, format_str_named, FormatterInvariant, clean_output, compile-equivalence, idempotent, assert_equivalent, structural_equivalent, strip_trailing_insignificant_text, in_raw_content, raw_content_spans, protected_spans, R1 R2 R3 R4, safety gate, token lossiness, @message body, @define body, @block body, FmtConfig, FmtFlags, interior-verbatim contract, try_scan_fence_at, FenceMatch, deep_merge_yaml, RESERVED_MERGE_KEYS, is_default_excluded_dir, is_within_default_excluded_dir, walker exclusions, node_modules, hidden dirs, effective_parent, bare filename, atomic_write_file.
- **mds-lint** — crates/mds-core/src/lint, crates/mds-cli/src, crates/mds-wasm/src, crates/mds-napi/src, crates/mds-python/src, packages/mds/src — Use when adding or modifying lint rules, extending the --fix pipeline, changing the JSON wire format, wiring lint into a binding layer, debugging unexpected exit codes and reverify gate refusals, or working on the ESC/bidi/newline injection defences. Keywords: mds lint, LintDiagnostic, fix_removals, fix_edits, TextEdit, FixLineSpan, diag_to_edits, LintResult, LintConfig, to_canonical_json, fix tier, reverify gate, FixOutcome, PartiallyFixed, apply_fixes_incremental, preview_fixes, PreviewOutcome, set_diag_display_path, AnalysisContext, ElseifBranch, end_offset, DefineFact, assertKnownKeys, CheckOptions, unreachable-branch, unused-variable, duplicate-import, empty-block, legacy-interpolation, is_output_neutral, all_output_neutral, Tier A Tier B Tier C, structural-standalone, compile-clean, is_standalone, sanitize_control_chars, sanitize_control_chars_wire, named_source_for_render, neutralize_source_for_render, SanitizedReport, SanitizedNode, MAX_AUX_DEPTH, EscapeMode, HUMAN WIRE, eprint_warning, safe_path, safe_inline, safe_file_display, preview_text_for, print_discipline, reverify_failure_reason, LintDirCtx, config_cache, dedup_contained_or_identical, EXIT 0 1 2 3, render_error_sanitized, eprint_error, display_sanitized, MdsError::display_sanitized, ESC-injection, CWE-150, CWE-117, bidi, Trojan-Source, CVE-2021-42574, U+061C, U+202E, U+FEFF, U+2028, U+2029, PF-014, PF-005, construction-time sanitization, per-field rule, Cow, #176.
- **source-map-security** — crates/mds-core/src, crates/mds-cli/src, packages/mds/src — Use when working with Source Map v3 generation, sources[] path relativization, the relativize_source choke-point, FileSystem::source_root(), CompileOptions.source_map_base, cross-surface source-map parity tests, or the Windows verbatim UNC path fix. Keywords: source map, sources[], relativize_source, source_map_base, source_root, path containment, basename fallback, PF-005, ADR-005, SEC-3, Windows verbatim UNC, path_to_unified, compute_source_map_base, apply_source_map_file_label, CF-SM2, V-SM1, differential test, two-level anchoring, map-relative, root-relative.
577 changes: 577 additions & 0 deletions .devflow/features/mds-lint/KNOWLEDGE.md

Large diffs are not rendered by default.

231 changes: 231 additions & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.lock

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

3 changes: 3 additions & 0 deletions crates/mds-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,8 @@ ctrlc = { workspace = true }
similar = { workspace = true }
tempfile = { workspace = true }

[dev-dependencies]
thiserror = { workspace = true }

[target.'cfg(unix)'.dev-dependencies]
libc = "0.2"
107 changes: 76 additions & 31 deletions crates/mds-cli/src/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ use std::io::Read;
use std::path::{Path, PathBuf};

use mds::{
effective_parent, sanitize_control_chars, CompiledOutput, MdsError, MAX_FILE_SIZE,
MAX_TRAVERSAL_DEPTH, STRING_SOURCE_MAP_LABEL,
effective_parent, CompiledOutput, MdsError, MAX_FILE_SIZE, MAX_TRAVERSAL_DEPTH,
STRING_SOURCE_MAP_LABEL,
};
use miette::Result;
use serde::Deserialize;
Expand Down Expand Up @@ -189,6 +189,18 @@ impl OutputKind {
}
}

/// Human-readable label for an [`OutputKind`], for the extension-mismatch warning.
///
/// Returns one of two `&'static str` literals and carries no runtime data — which is
/// why `kind_label(kind)` is allowlisted in `tests/print_discipline.rs` instead of
/// being wrapped in a sanitizer.
fn kind_label(kind: OutputKind) -> &'static str {
match kind {
OutputKind::Markdown => "markdown (.md)",
OutputKind::Messages => "messages JSON (.json)",
}
}

impl From<&CompiledOutput> for OutputKind {
fn from(output: &CompiledOutput) -> Self {
match output {
Expand Down Expand Up @@ -282,13 +294,19 @@ pub(crate) fn resolve_output_path_for_kind(
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
let expected = kind.extension();
if ext != expected {
// The `-o` value and the extension derived from it occupy a
// diagnostic `file` field on a status line: WIRE (spec §7.5
// per-field rule).
// The escape call is repeated rather than bound to a local so it
// is visible at each interpolation — the print-discipline guard
// reads call sites, not bindings.
eprintln!(
"warning: output path '{o}' has extension '.{ext}' but compiled \
output is {kind_name}; writing to '{o}' anyway",
kind_name = match kind {
OutputKind::Markdown => "markdown (.md)",
OutputKind::Messages => "messages JSON (.json)",
}
"warning: output path '{}' has extension '.{}' but compiled \
output is {}; writing to '{}' anyway",
crate::output::safe_inline(o),
crate::output::safe_inline(ext),
kind_label(kind),
crate::output::safe_inline(o)
);
}
}
Expand Down Expand Up @@ -561,7 +579,7 @@ pub(crate) fn write_output(
std::fs::write(&path, compiled)
.map_err(|e| miette::miette!("cannot write {}: {e}", path.display()))?;
if !quiet && announce {
eprintln!("Compiled to {}", path.display());
eprintln!("Compiled to {}", crate::output::safe_path(&path));
}
}
None => {
Expand Down Expand Up @@ -691,7 +709,7 @@ pub(crate) fn compile_to_content(

if !quiet {
for w in &result.warnings {
eprintln!("{w}");
crate::output::eprint_warning(w);
}
}

Expand Down Expand Up @@ -1003,7 +1021,7 @@ pub(crate) fn verify_then_delete_map(map_path: &Path, expected_basename: &str, q
if !quiet {
eprintln!(
"warning: leaving {} in place — not a tool-generated SMv3 map (version/file mismatch)",
map_path.display()
crate::output::safe_path(map_path)
);
}
return;
Expand All @@ -1012,20 +1030,21 @@ pub(crate) fn verify_then_delete_map(map_path: &Path, expected_basename: &str, q
if !quiet {
eprintln!(
"warning: leaving {} in place — not a tool-generated SMv3 map (version/file mismatch)",
map_path.display()
crate::output::safe_path(map_path)
);
}
return;
}
if let Err(e) = std::fs::remove_file(map_path) {
if !quiet {
eprintln!(
"warning: could not remove stale map {}: {e}",
map_path.display()
"warning: could not remove stale map {}: {}",
crate::output::safe_path(map_path),
crate::output::safe_inline(&e)
);
}
} else if !quiet {
eprintln!("Removed stale map {}", map_path.display());
eprintln!("Removed stale map {}", crate::output::safe_path(map_path));
}
}

Expand Down Expand Up @@ -1053,7 +1072,7 @@ pub(crate) fn run_build(args: BuildArgs) -> Result<()> {
// When auto-detected, print a "Building {path}" banner so users know which file was selected.
let (input, auto_detected) = resolve_input(input, "build")?;
if auto_detected && !quiet {
eprintln!("Building {}", input.display());
eprintln!("Building {}", crate::output::safe_path(&input));
}

// Directory mode: compile every non-partial .mds file in the tree.
Expand Down Expand Up @@ -1145,7 +1164,7 @@ pub(crate) fn run_build(args: BuildArgs) -> Result<()> {
.map_err(miette::Error::from)?;
if !quiet {
for w in &result.warnings {
eprintln!("{w}");
crate::output::eprint_warning(w);
}
}

Expand Down Expand Up @@ -1192,12 +1211,22 @@ pub(crate) fn run_build(args: BuildArgs) -> Result<()> {
write_output(Some(out.clone()), &content, quiet, true)?;
if let Some(ref sm) = source_map {
let map_path = map_path_for(out);
// The sidecar's `file` / `sources` / `sourcesContent` are
// written VERBATIM, by decision — spec §7.5 "Carve-out:
// functional path references". They are resolved against the
// filesystem by devtools and bundlers, so a `\uXXXX`-escaped
// path would not exist. Consumers must treat them as
// untrusted; see the `mds::SourceMap` rustdoc. The status
// line below is a diagnostic surface and IS escaped.
let map_json = sm.to_json();
std::fs::write(&map_path, &map_json).map_err(|e| {
miette::miette!("cannot write {}: {e}", map_path.display())
})?;
if !quiet {
eprintln!("Source map written to {}", map_path.display());
eprintln!(
"Source map written to {}",
crate::output::safe_path(&map_path)
);
}
}
return Ok(());
Expand Down Expand Up @@ -1310,9 +1339,10 @@ pub(crate) fn run_build(args: BuildArgs) -> Result<()> {
.map(|(_, p)| p.display().to_string())
.unwrap_or_else(|| "mds.json".to_owned());
eprintln!(
"warning: source_map in {cfg_path} has no effect when writing to \
"warning: source_map in {} has no effect when writing to \
stdout (sidecar requires -o <file> or --out-dir); use --inline to \
embed the map, or --no-source-map to silence this warning"
embed the map, or --no-source-map to silence this warning",
crate::output::safe_inline(&cfg_path)
);
}
return write_output(None, &compiled.content, quiet, true);
Expand All @@ -1326,7 +1356,10 @@ pub(crate) fn run_build(args: BuildArgs) -> Result<()> {
miette::miette!("cannot write {}: {e}", map_path.display())
})?;
if !quiet {
eprintln!("Source map written to {}", map_path.display());
eprintln!(
"Source map written to {}",
crate::output::safe_path(&map_path)
);
}
}
}
Expand Down Expand Up @@ -1418,7 +1451,7 @@ fn run_build_directory(
std::process::exit(1);
}
if !quiet {
eprintln!("No .mds files found in {}", dir.display());
eprintln!("No .mds files found in {}", crate::output::safe_path(dir));
}
return Ok(());
}
Expand Down Expand Up @@ -1466,8 +1499,9 @@ fn run_build_directory(
if !parent.as_os_str().is_empty() {
if let Err(e) = std::fs::create_dir_all(parent) {
eprintln!(
"error: cannot create output directory {}: {e}",
parent.display()
"error: cannot create output directory {}: {}",
crate::output::safe_path(parent),
crate::output::safe_inline(&e)
);
fail_count += 1;
continue;
Expand All @@ -1494,7 +1528,7 @@ fn run_build_directory(
match std::fs::write(&out_path, &final_content) {
Ok(()) => {
if !quiet {
eprintln!("Compiled to {}", out_path.display());
eprintln!("Compiled to {}", crate::output::safe_path(&out_path));
}
written_this_run.insert(out_path.clone());

Expand All @@ -1504,12 +1538,19 @@ fn run_build_directory(
let map_path = map_path_for(&out_path);
let map_json = sm.to_json();
if let Err(e) = std::fs::write(&map_path, &map_json) {
eprintln!("error: cannot write {}: {e}", map_path.display());
eprintln!(
"error: cannot write {}: {}",
crate::output::safe_path(&map_path),
crate::output::safe_inline(&e)
);
fail_count += 1;
continue;
}
if !quiet {
eprintln!("Source map written to {}", map_path.display());
eprintln!(
"Source map written to {}",
crate::output::safe_path(&map_path)
);
}
}
}
Expand Down Expand Up @@ -1537,15 +1578,19 @@ fn run_build_directory(
ok_count += 1;
}
Err(e) => {
eprintln!("error: cannot write {}: {e}", out_path.display());
eprintln!(
"error: cannot write {}: {}",
crate::output::safe_path(&out_path),
crate::output::safe_inline(&e)
);
fail_count += 1;
}
}
}
Err(e) => {
// Sanitize at the render boundary: MdsError::Syntax embeds user-controlled
// source fragments that may contain raw ESC bytes (avoids terminal escape injection).
eprintln!("{}", sanitize_control_chars(&format!("{e:?}")));
// Route through the single render choke point (avoids PF-004 /
// architecture-6: hand-rolled sanitize_control_chars bypass).
crate::output::eprint_error(e);
fail_count += 1;
}
}
Expand Down
Loading
Loading