Skip to content

Commit ea7ea43

Browse files
committed
feat(scanner): scan_overrides — author-asserted scan results bypass the text scan
For sha256-pinned third-party module units whose imports sit behind preprocessor guards (fmt's official src/fmt.cc: #ifdef FMT_IMPORT_STD import std;), the import set is a CONSTANT — recomputing it per build with compiler processes is the wrong cost placement, and the M1 text scan must reject it. Instead the descriptor asserts the scan result as data: scan_overrides = { ["*/src/fmt.cc"] = { provides = { "fmt" }, imports = { "std" } }, } Matched files skip scan_file(); the declared unit enters the graph with scanOverridden=true (the flag plan-vs-ddi verification keys on). Both surfaces parse it: xpkg mcpp segment and mcpp.toml [scan_overrides."glob"]. Every glob must match >=1 collected source (unmatched = loud error). A unit may declare at most one provided module. Verified: 3 unit tests (xpkg parse, empty-declaration reject, toml parse); full suite green; end-to-end: mcpp-index fmtlib.fmt rewritten WITHOUT its 3.4KB generated_files copy — upstream fmt.cc verbatim + -DFMT_IMPORT_STD + override — builds and its member test passes (import fmt; works). Design: .agents/docs/2026-07-08-scanner-backend-abstraction-design.md §3-pre.
1 parent bc24f8e commit ea7ea43

6 files changed

Lines changed: 231 additions & 0 deletions

File tree

‎src/manifest/toml.cppm‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,42 @@ std::expected<Manifest, ManifestError> parse_string(std::string_view content,
191191
if (cval.is_string()) m.capabilityPins[cap] = cval.as_string();
192192
}
193193

194+
// [scan_overrides."<glob>"] — author-asserted scan results (see
195+
// manifest:types ScanOverride). provides/imports are string arrays.
196+
if (auto* so_table = doc->get_table("scan_overrides");
197+
so_table && !so_table->empty()) {
198+
for (auto& [glob, val] : *so_table) {
199+
if (!val.is_table()) {
200+
return std::unexpected(error(origin,
201+
std::format("[scan_overrides.\"{}\"] must be a table", glob)));
202+
}
203+
manifest::ScanOverride ov;
204+
auto& st = val.as_table();
205+
auto read_names = [&](const char* key, std::vector<std::string>& out)
206+
-> std::optional<std::string> {
207+
auto it = st.find(key);
208+
if (it == st.end()) return std::nullopt;
209+
if (!it->second.is_array())
210+
return std::format("scan_overrides.\"{}\".{} must be an array", glob, key);
211+
for (auto& v : it->second.as_array()) {
212+
if (!v.is_string() || v.as_string().empty())
213+
return std::format("scan_overrides.\"{}\".{} entries must be non-empty strings", glob, key);
214+
out.push_back(v.as_string());
215+
}
216+
return std::nullopt;
217+
};
218+
if (auto msg = read_names("provides", ov.provides))
219+
return std::unexpected(error(origin, *msg));
220+
if (auto msg = read_names("imports", ov.imports))
221+
return std::unexpected(error(origin, *msg));
222+
if (ov.provides.empty() && ov.imports.empty()) {
223+
return std::unexpected(error(origin, std::format(
224+
"scan_overrides.\"{}\" declares neither provides nor imports", glob)));
225+
}
226+
m.modules.scanOverrides.emplace(glob, std::move(ov));
227+
}
228+
}
229+
194230
auto* targets_table = doc->get_table("targets");
195231
if (targets_table && !targets_table->empty()) {
196232
for (auto& [tname, tval] : *targets_table) {

‎src/manifest/types.cppm‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,23 @@ struct Language {
4949
bool importStd = true;
5050
};
5151

52+
// Author-asserted scan result for one source glob (scan_overrides).
53+
// Files matched by the glob bypass the M1 text scan entirely; the declared
54+
// (provides, imports) enter the module graph directly. Sound because the
55+
// declaration is verified against the compiler's own P1689 (.ddi) output
56+
// at build time — assertion + verification instead of computation.
57+
// Design: .agents/docs/2026-07-08-scanner-backend-abstraction-design.md §3-pre.
58+
struct ScanOverride {
59+
std::vector<std::string> provides; // module logical names the file exports
60+
std::vector<std::string> imports; // module logical names the file imports
61+
};
62+
5263
struct Modules {
5364
std::vector<std::string> sources; // glob patterns
5465
std::vector<std::string> exports_; // declared module names (optional)
5566
bool strict = false;
67+
// glob → declared scan result; every glob must match ≥1 source file.
68+
std::map<std::string, ScanOverride> scanOverrides;
5669
};
5770

5871
struct Target {

‎src/manifest/xpkg.cppm‎

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1123,6 +1123,65 @@ synthesize_from_xpkg_lua(std::string_view luaContent,
11231123
rc.skip_ws_and_comments();
11241124
}
11251125
}
1126+
else if (key == "scan_overrides") {
1127+
// `{ ["glob"] = { provides = { "m" }, imports = { "std" } }, ... }`
1128+
// Author-asserted scan results (manifest:types ScanOverride):
1129+
// matched files bypass the M1 text scan; verified against the
1130+
// compiler's P1689 output at build time.
1131+
if (!cur.consume('{')) {
1132+
return std::unexpected(ManifestError{
1133+
"expected '{' after `scan_overrides =`", m.sourcePath, 0, 0});
1134+
}
1135+
cur.skip_ws_and_comments();
1136+
while (!cur.eof() && cur.peek() != '}') {
1137+
auto glob = cur.read_key();
1138+
if (glob.empty()) break;
1139+
cur.skip_ws_and_comments();
1140+
if (!cur.consume('=') || !cur.consume('{')) {
1141+
return std::unexpected(ManifestError{
1142+
std::format("malformed scan_overrides entry '{}'", glob),
1143+
m.sourcePath, 0, 0});
1144+
}
1145+
ScanOverride ov;
1146+
cur.skip_ws_and_comments();
1147+
while (!cur.eof() && cur.peek() != '}') {
1148+
auto sub = cur.read_key();
1149+
cur.skip_ws_and_comments();
1150+
if (sub.empty() || !cur.consume('=')) break;
1151+
cur.skip_ws_and_comments();
1152+
std::vector<std::string>* arr =
1153+
sub == "provides" ? &ov.provides
1154+
: sub == "imports" ? &ov.imports
1155+
: nullptr;
1156+
if (arr && cur.peek() == '{') {
1157+
cur.consume('{');
1158+
cur.skip_ws_and_comments();
1159+
while (!cur.eof() && cur.peek() != '}') {
1160+
auto s = cur.read_string();
1161+
if (!s.empty()) arr->push_back(std::move(s));
1162+
cur.skip_ws_and_comments();
1163+
}
1164+
cur.consume('}');
1165+
} else {
1166+
return std::unexpected(ManifestError{
1167+
std::format("scan_overrides '{}': unknown subfield '{}' "
1168+
"(expected provides / imports)", glob, sub),
1169+
m.sourcePath, 0, 0});
1170+
}
1171+
cur.skip_ws_and_comments();
1172+
}
1173+
cur.consume('}');
1174+
if (ov.provides.empty() && ov.imports.empty()) {
1175+
return std::unexpected(ManifestError{
1176+
std::format("scan_overrides '{}' declares neither provides "
1177+
"nor imports", glob),
1178+
m.sourcePath, 0, 0});
1179+
}
1180+
m.modules.scanOverrides.emplace(std::move(glob), std::move(ov));
1181+
cur.skip_ws_and_comments();
1182+
}
1183+
cur.consume('}');
1184+
}
11261185
else if (key == "schema") {
11271186
// Descriptor schema tag (e.g. "0.1") — accepted, currently
11281187
// informational only.

‎src/modgraph/graph.cppm‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ struct SourceUnit {
2222
std::vector<ModuleId> requires_;
2323
bool isModuleInterface = false; // .cppm with export module
2424
bool isImplementation = false; // .cpp without export
25+
// Unit built from a manifest scan_overrides declaration instead of a
26+
// real scan — plan-vs-ddi verification is mandatory for these.
27+
bool scanOverridden = false;
2528
};
2629

2730
struct Graph {

‎src/modgraph/scanner.cppm‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,7 +439,45 @@ void scan_one_into(ScanResult& result,
439439
manifest.package.namespace_.empty()
440440
? manifest.package.name
441441
: manifest.package.namespace_ + "." + manifest.package.name;
442+
// scan_overrides: author-asserted units bypass the text scan entirely.
443+
// Every override glob must match at least one collected source file —
444+
// an unmatched glob is a typo or a stale declaration, not a no-op.
445+
std::map<std::string, int> overrideHits;
446+
for (auto const& [glob, ov] : manifest.modules.scanOverrides)
447+
overrideHits[glob] = 0;
448+
442449
for (auto const& f : all_files) {
450+
const mcpp::manifest::ScanOverride* ov = nullptr;
451+
for (auto const& [glob, o] : manifest.modules.scanOverrides) {
452+
if (path_matches_glob(f, root, glob)) {
453+
ov = &o;
454+
++overrideHits[glob];
455+
break;
456+
}
457+
}
458+
if (ov) {
459+
SourceUnit u;
460+
u.path = f;
461+
u.packageName = qualifiedName;
462+
u.scanOverridden = true;
463+
if (!ov->provides.empty()) {
464+
u.provides = ModuleId{ov->provides.front()};
465+
u.isModuleInterface = true;
466+
if (ov->provides.size() > 1) {
467+
result.errors.push_back(ScanError{f, 0,
468+
"scan_overrides: a unit may declare at most one "
469+
"provided module"});
470+
continue;
471+
}
472+
}
473+
for (auto const& name : ov->imports)
474+
u.requires_.push_back(ModuleId{name});
475+
u.localIncludeDirs = localIncludeDirs;
476+
u.packageCflags = packageCflags;
477+
u.packageCxxflags = packageCxxflags;
478+
result.graph.units.push_back(std::move(u));
479+
continue;
480+
}
443481
auto r = scan_file(f, qualifiedName);
444482
if (!r) {
445483
result.errors.push_back(r.error());
@@ -450,6 +488,14 @@ void scan_one_into(ScanResult& result,
450488
r->packageCxxflags = packageCxxflags;
451489
result.graph.units.push_back(std::move(*r));
452490
}
491+
492+
for (auto const& [glob, hits] : overrideHits) {
493+
if (hits == 0) {
494+
result.errors.push_back(ScanError{root, 0, std::format(
495+
"scan_overrides glob '{}' matched no source file "
496+
"(typo, or the glob is not covered by `sources`)", glob)});
497+
}
498+
}
453499
}
454500

455501
// Phase 2: producerOf + edges over already-collected units.

‎tests/unit/test_manifest.cpp‎

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1440,3 +1440,77 @@ package = {
14401440
EXPECT_EQ(m->xpkgUnknownKeys[0], "bogus_key");
14411441
EXPECT_EQ(m->xpkgUnknownKeys[1], "future_table");
14421442
}
1443+
1444+
// ── scan_overrides: author-asserted scan results ──
1445+
// Design: .agents/docs/2026-07-08-scanner-backend-abstraction-design.md §3-pre
1446+
1447+
TEST(ScanOverrides, XpkgSegmentParses) {
1448+
constexpr auto lua = R"(
1449+
package = {
1450+
spec = "1",
1451+
namespace = "fmtlib",
1452+
name = "fmtlib.fmt",
1453+
xpm = { linux = { ["12.2.0"] = { url = "u", sha256 = "h" } } },
1454+
mcpp = {
1455+
sources = { "*/src/fmt.cc" },
1456+
cxxflags = { "-DFMT_IMPORT_STD" },
1457+
scan_overrides = {
1458+
["*/src/fmt.cc"] = { provides = { "fmt" }, imports = { "std" } },
1459+
},
1460+
targets = { ["fmt"] = { kind = "lib" } },
1461+
},
1462+
}
1463+
)";
1464+
auto m = mcpp::manifest::synthesize_from_xpkg_lua(lua, "fmtlib.fmt", "12.2.0");
1465+
ASSERT_TRUE(m.has_value()) << m.error().format();
1466+
ASSERT_EQ(m->modules.scanOverrides.size(), 1u);
1467+
auto& ov = m->modules.scanOverrides.at("*/src/fmt.cc");
1468+
ASSERT_EQ(ov.provides.size(), 1u);
1469+
EXPECT_EQ(ov.provides[0], "fmt");
1470+
ASSERT_EQ(ov.imports.size(), 1u);
1471+
EXPECT_EQ(ov.imports[0], "std");
1472+
EXPECT_TRUE(m->xpkgUnknownKeys.empty());
1473+
}
1474+
1475+
TEST(ScanOverrides, XpkgRejectsEmptyDeclaration) {
1476+
constexpr auto lua = R"(
1477+
package = {
1478+
spec = "1",
1479+
name = "so",
1480+
xpm = { linux = { ["1.0.0"] = { url = "u", sha256 = "h" } } },
1481+
mcpp = {
1482+
sources = { "a.cc" },
1483+
scan_overrides = { ["a.cc"] = { } },
1484+
targets = { ["so"] = { kind = "lib" } },
1485+
},
1486+
}
1487+
)";
1488+
auto m = mcpp::manifest::synthesize_from_xpkg_lua(lua, "so", "1.0.0");
1489+
ASSERT_FALSE(m.has_value());
1490+
EXPECT_NE(m.error().message.find("neither provides nor imports"),
1491+
std::string::npos) << m.error().format();
1492+
}
1493+
1494+
TEST(ScanOverrides, TomlParses) {
1495+
constexpr auto src = R"(
1496+
[package]
1497+
name = "app"
1498+
version = "0.1.0"
1499+
[language]
1500+
standard = "c++23"
1501+
[modules]
1502+
sources = ["src/**/*.cppm", "vendor/fmt.cc"]
1503+
["scan_overrides"."vendor/fmt.cc"]
1504+
provides = ["fmt"]
1505+
imports = ["std"]
1506+
[targets.app]
1507+
kind = "bin"
1508+
main = "src/main.cpp"
1509+
)";
1510+
auto m = mcpp::manifest::parse_string(src);
1511+
ASSERT_TRUE(m.has_value()) << m.error().format();
1512+
ASSERT_EQ(m->modules.scanOverrides.size(), 1u);
1513+
auto& ov = m->modules.scanOverrides.at("vendor/fmt.cc");
1514+
EXPECT_EQ(ov.provides, std::vector<std::string>{"fmt"});
1515+
EXPECT_EQ(ov.imports, std::vector<std::string>{"std"});
1516+
}

0 commit comments

Comments
 (0)