kicad-validity B3+C+S: revert toast, corpus lint, .kicad_mod tier

B3 (editor): kicad-binding observes kdoc_meta.revertNonce (deduped, like
seedNonce) → DOC_REVERTED_EVENT → WasmTool doc-reverted-toast (15 s,
data-testid for the deferred e2e check).
C: tests/tools/corpus-lint.ts (npm run corpus:lint) — kicad_tools --lint
over the fixture corpora raw + shared-codec round-trips
(docToFile(fileToDoc(x))); the E3/wrapInBoardEnvelope-class gate. Skips
when output/kicad_tools.js isn't built; CI step lands when kicad_tools
joins the CI app set. 7 fixtures / 7 round-trips green.
S: --lint + --resave gain the .kicad_mod tier (pcbToolsLintFootprint /
pcbToolsResaveFootprint; writer needs CTL_FOR_LIBRARY or the board flags
strip the (version) header). Verified: qa footprint round-trip lints
clean, resave stamps 20260206, garbage exits 1. Also bumps pcbjam-shared
(mergeYUpdates for the bisect pass).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HLua64PCVwkQ1hpWdaf1Gm
This commit is contained in:
Gergő Törcsvári 2026-07-14 10:57:46 +02:00
commit fb2aa2759d
No known key found for this signature in database
GPG key ID: 8E75F2CDE64E5322
8 changed files with 323 additions and 7 deletions

View file

@ -14,9 +14,9 @@
* eeschema side (sym_convert_main.cpp) — everything else:
* kicad_tools --convert-lib <input.lib> <output.kicad_sym>
* kicad_tools --lint [--strict] <file> [<file>...]
* (.kicad_pcb files get a FULL parse here — the pcbnew
* parser is linked; the lint driver calls back into
* pcbToolsLintBoard on the pcb side)
* (.kicad_pcb/.kicad_mod get a FULL parse here — the
* pcbnew parser is linked; the lint driver calls back
* into pcbToolsLintBoard/pcbToolsLintFootprint)
* kicad_tools --resave <file> <outdir>
* (full parse + rewrite in the current file-format
* version — kicad-validity 0001; .kicad_pcb via the

View file

@ -69,8 +69,11 @@
#include <layer_ids.h>
#include <lset.h>
#include <libraries/library_manager.h>
#include <footprint.h>
#include <pcb_io/kicad_sexpr/pcb_io_kicad_sexpr.h>
#include <pcb_io/pcb_io_mgr.h>
#include <pcb_marker.h>
#include <richio.h>
#include <pcb_plotter.h>
#include <pcbplot.h>
#include <pgm_base.h>
@ -681,6 +684,120 @@ int pcbToolsLintBoard( const char* aInPath, std::string& aError )
}
// Full-parse footprint lint for the merged image's --lint driver
// (kicad-validity 0001 S): parse one .kicad_mod via the s-expr plugin's
// ImportFootprint. Returns the pad count, or -1 with aError filled.
int pcbToolsLintFootprint( const char* aInPath, std::string& aError )
{
wxFileName fn( wxString::FromUTF8( aInPath ) );
fn.MakeAbsolute();
kiRuntime();
try
{
PCB_IO_KICAD_SEXPR plugin;
wxString name;
std::unique_ptr<FOOTPRINT> fp( plugin.ImportFootprint( fn.GetFullPath(), name ) );
if( !fp )
{
aError = std::string( aInPath ) + ": error: not a footprint file";
return -1;
}
return (int) fp->Pads().size();
}
catch( PARSE_ERROR& pe )
{
char buf[1024];
std::snprintf( buf, sizeof( buf ), "%s:%d:%d: error: %s", aInPath, pe.lineNumber,
pe.byteIndex, (const char*) pe.ParseProblem().ToUTF8() );
aError = buf;
return -1;
}
catch( const IO_ERROR& ioe )
{
aError = std::string( aInPath ) + ": error: " + std::string( ioe.Problem().ToUTF8() );
return -1;
}
catch( const std::exception& e )
{
aError = std::string( aInPath ) + ": error: " + e.what();
return -1;
}
}
// Footprint resave for --resave (kicad-validity 0001 S): parse + rewrite one
// .kicad_mod in the current format, the exact FP_CACHE::Save incantation
// (PRETTIFIED formatter + plugin Format). 0 / 4 load failed / 5 write failed.
int pcbToolsResaveFootprint( const char* aInPath, const char* aOutPath, std::string& aError )
{
wxFileName fn( wxString::FromUTF8( aInPath ) );
fn.MakeAbsolute();
kiRuntime();
std::unique_ptr<FOOTPRINT> fp;
try
{
PCB_IO_KICAD_SEXPR plugin;
wxString name;
fp.reset( plugin.ImportFootprint( fn.GetFullPath(), name ) );
if( !fp )
{
aError = std::string( aInPath ) + ": error: not a footprint file";
return 4;
}
}
catch( PARSE_ERROR& pe )
{
char buf[1024];
std::snprintf( buf, sizeof( buf ), "%s:%d:%d: error: %s", aInPath, pe.lineNumber,
pe.byteIndex, (const char*) pe.ParseProblem().ToUTF8() );
aError = buf;
return 4;
}
catch( const IO_ERROR& ioe )
{
aError = std::string( aInPath ) + ": error: " + std::string( ioe.Problem().ToUTF8() );
return 4;
}
catch( const std::exception& e )
{
aError = std::string( aInPath ) + ": error: " + e.what();
return 4;
}
try
{
// CTL_FOR_LIBRARY, NOT the default CTL_FOR_BOARD — the board flags
// include CTL_OMIT_FOOTPRINT_VERSION, which would strip the (version)
// header a standalone .kicad_mod must carry (FootprintSave sets the
// same flags before formatting).
PCB_IO_KICAD_SEXPR writer( CTL_FOR_LIBRARY );
PRETTIFIED_FILE_OUTPUTFORMATTER formatter( wxString::FromUTF8( aOutPath ) );
writer.SetOutputFormatter( &formatter );
writer.Format( fp.get() );
}
catch( const IO_ERROR& ioe )
{
aError = std::string( aOutPath ) + ": error: " + std::string( ioe.Problem().ToUTF8() );
return 5;
}
catch( const std::exception& e )
{
aError = std::string( aOutPath ) + ": error: " + e.what();
return 5;
}
return 0;
}
// Full-parse board resave for the merged image's --resave driver (also on the
// eeschema side): a bare load like pcbToolsLintBoard, then the s-expr writer
// rewrites the board in the current format version. Zones are written

View file

@ -102,6 +102,10 @@ int pcbToolsLintBoard( const char* aInPath, std::string& aError );
// Merged image only: board load + s-expr rewrite for --resave (kicad-validity
// 0001). 0 resaved / 4 load failed / 5 write failed.
int pcbToolsResaveBoard( const char* aInPath, const char* aOutPath, std::string& aError );
// Merged image only: .kicad_mod full parse (returns pad count or -1) and
// resave (0/4/5) — kicad-validity 0001 S.
int pcbToolsLintFootprint( const char* aInPath, std::string& aError );
int pcbToolsResaveFootprint( const char* aInPath, const char* aOutPath, std::string& aError );
#endif
#include <connection_graph.h>
@ -539,6 +543,15 @@ bool lintOneFile( const char* aPath, bool aStrict )
else
tier = "full parse";
}
else if( ext == wxS( "kicad_mod" ) )
{
std::string fpError;
if( pcbToolsLintFootprint( aPath, fpError ) < 0 )
report.errors.push_back( fpError );
else
tier = "full parse";
}
#endif
// Anything else (kicad_wks, kicad_pro …— and kicad_pcb outside
// the merged kicad_tools image) stays structure-only.
@ -813,12 +826,14 @@ int runResave( const char* aInPath, const char* aOutDir )
return 0;
}
if( ext == wxS( "kicad_pcb" ) )
if( ext == wxS( "kicad_pcb" ) || ext == wxS( "kicad_mod" ) )
{
#ifdef KICAD_TOOLS_COMBINED
const wxString outPath = outDir + inFn.GetFullName();
std::string error;
const int rc = pcbToolsResaveBoard( aInPath, (const char*) outPath.ToUTF8(), error );
const int rc = ext == wxS( "kicad_pcb" )
? pcbToolsResaveBoard( aInPath, (const char*) outPath.ToUTF8(), error )
: pcbToolsResaveFootprint( aInPath, (const char*) outPath.ToUTF8(), error );
if( rc != 0 )
std::fprintf( stderr, "%s\n", error.c_str() );
@ -828,7 +843,8 @@ int runResave( const char* aInPath, const char* aOutDir )
return rc;
#else
std::fprintf( stderr, "%s: error: board resave requires the merged kicad_tools build\n",
std::fprintf( stderr, "%s: error: board/footprint resave requires the merged "
"kicad_tools build\n",
aInPath );
return 2;
#endif