Skip to content

Commit bed05b9

Browse files
committed
Complete upstream CLI follow-up integration
1 parent 1dfc685 commit bed05b9

15 files changed

Lines changed: 525 additions & 75 deletions

‎docs/CLI_UPSTREAM_FOLLOWUPS/CP90_FOLLOWUP_COMPLETION_PLAN.md‎

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ merging MCP frontend code or pushing worker branches.
1111

1212
| Workstream | Worker result | Implementation evidence | Evidence gap |
1313
| --- | --- | --- | --- |
14-
| YAML removal guards | blocked | 523d398d; Windows-gated reparse-point and access-denied tests; 64 YAML tests passed | Native Windows execution; completion sidecar was initially read-only |
15-
| Version-cohort handoff | blocked | 01af5cb2; distinct cache-root handoff and Windows separator-form tests | Runtime assertions could not create secure temp roots via cbm_mkdtemp |
16-
| Complexity determinism | partial | 43d3a210; production build and changed objects compiled | Focused pipeline suite was interrupted while generated grammar objects stalled |
17-
| Razor / .cshtml | blocked | 5b1bc085; .cshtml language mapping, route extraction, and three regressions; 562 language/extraction tests passed | Completion sidecar was not finalized; native Windows is not required for this feature |
18-
| Embedded Svelte/HTML/Astro | partial | f8971e7a; implementation and focused tests committed; whitespace check passed | Test runner link failed on missing SQLite symbols, so extraction suites did not execute |
14+
| YAML removal guards | complete on Linux | 523d398d; Windows-gated reparse-point and access-denied tests; focused suite passed | Native Windows execution remains deferred |
15+
| Version-cohort handoff | complete on Linux | 01af5cb2; distinct cache-root handoff and Windows separator-form tests; focused suite passed | Native Windows execution remains deferred |
16+
| Complexity determinism | complete | 43d3a210; canonical tie-breaker and fixture corpus; pipeline suite passed | None |
17+
| Razor / .cshtml | complete | 5b1bc085; .cshtml language mapping, route extraction, and three regressions; focused suites passed | None |
18+
| Embedded Svelte/HTML/Astro | complete on Linux | f8971e7a; corrected test-runner linkage and focused extraction/structural/label suites passed | Native Windows execution is not required for this feature |
1919

2020
The separate complexity-r2 run is not a sixth task. It only rechecked the
2121
complexity fixture and was blocked while writing its completion sidecar; its
@@ -167,13 +167,15 @@ qualification is required for Windows-specific claims. Commit the accepted
167167
integration as a separate release-tooling change; do not push until the full
168168
gate and release review are green.
169169

170-
## Explicit deferrals
170+
## Remaining qualification deferrals
171171

172172
- 92725a5e remains excluded: it is a Windows MCP stdio-server watchdog, not a
173173
CLI process behavior.
174174
- c1b9c451 remains excluded as a direct port; its MCP session adapter is
175175
obsolete. Any 9846c3f1 separator-equivalence behavior requires a separate
176176
neutral daemon implementation and test.
177-
- 24f54c0e is provisionally implemented in f8971e7a, but acceptance is blocked
178-
on complete focused-suite linkage and review of the intentional graph/label
179-
behavior changes. Revert or defer it if those gates cannot be met.
177+
- Native Windows execution of the YAML and cohort cases remains outstanding;
178+
Linux cannot substitute for that evidence.
179+
- 24f54c0e is accepted on Linux in f8971e7a after the focused runner linked and
180+
passed; its intentional graph/label behavior changes are covered by the
181+
focused suites.

‎docs/WINDOWS_BENCHMARK_RUNBOOK.md‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,41 @@ Benchmarking is only one part of the Windows RC gate. For the same exact executa
154154
8. generate `qualification-summary.md` and `qualification-manifest.json` against the exact artifact hashes;
155155
9. attach evidence to the draft release and use the existing promotion workflow.
156156

157+
## Follow-up qualification batch
158+
159+
The CP90 Windows follow-ups can be exercised in one native-Windows run. Build
160+
the source test runner once, then run both C follow-up suites, the existing
161+
native product guards, and the benchmark corpus without rebuilding between
162+
steps. Run this from a clean checkout when source-level test evidence is
163+
required; the RC orchestrator above remains the authority for immutable
164+
release-artifact evidence.
165+
166+
```powershell
167+
$ErrorActionPreference = 'Stop'
168+
$make = (Get-Command make).Source
169+
& $make '-j' '-f' 'Makefile.cbm' 'build/c/test-runner' 'SANITIZE=' `
170+
'TMP=' + $env:TEMP 'TEMP=' + $env:TEMP 'TMPDIR=' + $env:TEMP
171+
if ($LASTEXITCODE) { throw "test-runner build failed ($LASTEXITCODE)" }
172+
173+
& 'build/c/test-runner' 'config_yaml_edit' 'version_cohort'
174+
if ($LASTEXITCODE) { throw "CP90 C follow-up tests failed ($LASTEXITCODE)" }
175+
176+
& '.\scripts\test-windows.ps1' -GuardsOnly -Binary `
177+
'build\c\codebase-memory-cli.exe'
178+
if ($LASTEXITCODE) { throw "native Windows guards failed ($LASTEXITCODE)" }
179+
180+
& '.\scripts\qualification\run-windows-benchmark-corpus.ps1' `
181+
-CandidateBinary 'build\c\codebase-memory-cli.exe' `
182+
-ResultsRoot 'C:\cbm-benchmark\results\cp90-followup'
183+
if ($LASTEXITCODE) { throw "benchmark corpus failed ($LASTEXITCODE)" }
184+
```
185+
186+
The YAML reparse-point/access-denied and cohort separator cases are the
187+
Windows-specific CP90 evidence. Record the complete console output and the
188+
timestamped benchmark result directory. A source-built executable is suitable
189+
for follow-up qualification only; it must not replace the exact published RC
190+
bytes used by the release qualification sequence.
191+
157192
## Manual fallback
158193

159194
To benchmark one repository without the orchestrator:

‎internal/cbm/extract_defs.c‎

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7902,6 +7902,73 @@ void cbm_extract_definitions_without_module(CBMExtractCtx *ctx) {
79027902
extract_variables(ctx, ctx->root, spec);
79037903
}
79047904

7905+
/* Razor's @page directive is outside the recovered C# AST. */
7906+
static bool cbm_path_is_razor(const char *rel_path) {
7907+
if (!rel_path) {
7908+
return false;
7909+
}
7910+
static const char *const suffixes[] = {".razor", ".cshtml"};
7911+
size_t len = strlen(rel_path);
7912+
for (size_t i = 0; i < sizeof(suffixes) / sizeof(suffixes[0]); i++) {
7913+
size_t slen = strlen(suffixes[i]);
7914+
if (len > slen && strcmp(rel_path + len - slen, suffixes[i]) == 0) {
7915+
return true;
7916+
}
7917+
}
7918+
return false;
7919+
}
7920+
7921+
static const char *razor_page_route_on_line(CBMArena *a, const char *line,
7922+
const char *line_end) {
7923+
static const char directive[] = "@page";
7924+
const size_t dlen = sizeof(directive) - 1U;
7925+
const char *p = line;
7926+
while (p < line_end && (*p == ' ' || *p == '\t')) {
7927+
p++;
7928+
}
7929+
if (p == line_end || (size_t)(line_end - p) <= dlen ||
7930+
strncmp(p, directive, dlen) != 0) {
7931+
return NULL;
7932+
}
7933+
p += dlen;
7934+
if (p == line_end || (*p != ' ' && *p != '\t')) {
7935+
return NULL;
7936+
}
7937+
while (p < line_end && (*p == ' ' || *p == '\t')) {
7938+
p++;
7939+
}
7940+
if (p == line_end || *p != '"') {
7941+
return NULL;
7942+
}
7943+
const char *route = ++p;
7944+
while (p < line_end && *p != '"') {
7945+
p++;
7946+
}
7947+
if (p == line_end || p == route || *route != '/') {
7948+
return NULL;
7949+
}
7950+
return cbm_arena_strndup(a, route, (size_t)(p - route));
7951+
}
7952+
7953+
static const char *cbm_razor_page_route(CBMArena *a, const char *source, int source_len) {
7954+
if (!source || source_len <= 0) {
7955+
return NULL;
7956+
}
7957+
const char *end = source + source_len;
7958+
for (const char *line = source; line < end;) {
7959+
const char *nl = memchr(line, '\n', (size_t)(end - line));
7960+
const char *route = razor_page_route_on_line(a, line, nl ? nl : end);
7961+
if (route) {
7962+
return route;
7963+
}
7964+
if (!nl) {
7965+
break;
7966+
}
7967+
line = nl + 1;
7968+
}
7969+
return NULL;
7970+
}
7971+
79057972
void cbm_extract_definitions(CBMExtractCtx *ctx) {
79067973
const CBMLangSpec *spec = cbm_lang_spec(ctx->language);
79077974
if (!spec) {
@@ -7923,6 +7990,13 @@ void cbm_extract_definitions(CBMExtractCtx *ctx) {
79237990
mod.is_test = ctx->result->is_test_file;
79247991
// #519: index what a config file declares itself to be, not only its path.
79257992
mod.docstring = extract_config_module_description(ctx);
7993+
if (ctx->language == CBM_LANG_CSHARP && cbm_path_is_razor(ctx->rel_path)) {
7994+
const char *route = cbm_razor_page_route(a, ctx->source, ctx->source_len);
7995+
if (route) {
7996+
mod.route_path = route;
7997+
mod.route_method = "GET";
7998+
}
7999+
}
79268000
cbm_defs_push(&ctx->result->defs, a, mod);
79278001

79288002
cbm_extract_definitions_without_module(ctx);

‎internal/cbm/extract_imports.c‎

Lines changed: 54 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1383,7 +1383,7 @@ static void parse_spec_imports(CBMExtractCtx *ctx) {
13831383
ts_tree_cursor_delete(&cursor);
13841384
}
13851385

1386-
// --- Embedded-language imports ---
1386+
// --- Embedded-language structure and imports ---
13871387
// Generic walker for host grammars (Svelte, Vue, HTML, Astro, ...) whose AST
13881388
// keeps embedded sub-language source as raw_text (or similar) without parsing
13891389
// it. The host's CBMLangSpec.embedded_imports declares which content nodes
@@ -1462,62 +1462,74 @@ static bool ascii_trimmed_equals(const char *value, const char *expected) {
14621462
* its inline programs to the language forms whose parsers we can select
14631463
* exactly. A src attribute always denotes an external program and therefore
14641464
* suppresses any inline extraction, even for malformed mixed markup. */
1465-
static bool vue_embedded_language(CBMExtractCtx *ctx, TSNode script, CBMLanguage *language) {
1466-
enum { VUE_ATTR_STACK_CAP = 128 };
1467-
TSNode stack[VUE_ATTR_STACK_CAP];
1465+
static const char *script_attribute_value(CBMExtractCtx *ctx, TSNode attribute) {
1466+
TSNode value_node = attribute;
1467+
if (!find_first_descendant_of(attribute, "attribute_value", &value_node)) {
1468+
return "";
1469+
}
1470+
return cbm_node_text(ctx->arena, value_node, ctx->source);
1471+
}
1472+
1473+
static bool apply_script_attribute(CBMExtractCtx *ctx, TSNode attribute, CBMLanguage *language) {
1474+
if (ts_node_named_child_count(attribute) == 0) {
1475+
return false;
1476+
}
1477+
const char *name = cbm_node_text(ctx->arena, ts_node_named_child(attribute, 0), ctx->source);
1478+
if (ascii_trimmed_equals(name, "src")) {
1479+
return false;
1480+
}
1481+
if (ascii_trimmed_equals(name, "lang")) {
1482+
const char *value = script_attribute_value(ctx, attribute);
1483+
if (ascii_trimmed_equals(value, "js") || ascii_trimmed_equals(value, "javascript")) {
1484+
*language = CBM_LANG_JAVASCRIPT;
1485+
return true;
1486+
}
1487+
if (ascii_trimmed_equals(value, "ts") || ascii_trimmed_equals(value, "typescript")) {
1488+
*language = CBM_LANG_TYPESCRIPT;
1489+
return true;
1490+
}
1491+
return false;
1492+
}
1493+
if (ascii_trimmed_equals(name, "type")) {
1494+
const char *value = script_attribute_value(ctx, attribute);
1495+
return ascii_trimmed_equals(value, "") || ascii_trimmed_equals(value, "module") ||
1496+
ascii_trimmed_equals(value, "text/javascript") ||
1497+
ascii_trimmed_equals(value, "application/javascript");
1498+
}
1499+
return true;
1500+
}
1501+
1502+
static bool script_embedded_language(CBMExtractCtx *ctx, TSNode script, CBMLanguage *language) {
1503+
enum { SCRIPT_ATTR_STACK_CAP = 128 };
1504+
TSNode stack[SCRIPT_ATTR_STACK_CAP];
14681505
int top = 0;
1469-
bool has_lang = false;
1470-
bool lang_supported = true;
1471-
CBMLanguage selected = CBM_LANG_JAVASCRIPT;
14721506
stack[top++] = script;
14731507
while (top > 0) {
14741508
TSNode node = stack[--top];
14751509
if (strcmp(ts_node_type(node), "attribute") == 0) {
1476-
uint32_t named_count = ts_node_named_child_count(node);
1477-
if (named_count == 0) {
1478-
return false;
1479-
}
1480-
TSNode name_node = ts_node_named_child(node, 0);
1481-
char *name = cbm_node_text(ctx->arena, name_node, ctx->source);
1482-
if (ascii_trimmed_equals(name, "src")) {
1510+
if (!apply_script_attribute(ctx, node, language)) {
14831511
return false;
14841512
}
1485-
if (ascii_trimmed_equals(name, "lang")) {
1486-
TSNode value_node = node;
1487-
if (!find_first_descendant_of(node, "attribute_value", &value_node)) {
1488-
return false;
1489-
}
1490-
char *value = cbm_node_text(ctx->arena, value_node, ctx->source);
1491-
has_lang = true;
1492-
if (ascii_trimmed_equals(value, "js") ||
1493-
ascii_trimmed_equals(value, "javascript")) {
1494-
selected = CBM_LANG_JAVASCRIPT;
1495-
} else if (ascii_trimmed_equals(value, "ts") ||
1496-
ascii_trimmed_equals(value, "typescript")) {
1497-
selected = CBM_LANG_TYPESCRIPT;
1498-
} else {
1499-
lang_supported = false;
1500-
}
1501-
}
15021513
continue;
15031514
}
15041515
uint32_t count = ts_node_named_child_count(node);
1505-
if ((int)count > VUE_ATTR_STACK_CAP - top) {
1516+
if ((int)count > SCRIPT_ATTR_STACK_CAP - top) {
15061517
return false;
15071518
}
15081519
for (int i = (int)count - 1; i >= 0; i--) {
15091520
stack[top++] = ts_node_named_child(node, (uint32_t)i);
15101521
}
15111522
}
1512-
if (has_lang && !lang_supported) {
1513-
return false;
1514-
}
1515-
*language = selected;
15161523
return true;
15171524
}
15181525

1526+
static bool markup_script_host(CBMLanguage language) {
1527+
return language == CBM_LANG_VUE || language == CBM_LANG_SVELTE || language == CBM_LANG_HTML ||
1528+
language == CBM_LANG_ASTRO;
1529+
}
1530+
15191531
static void parse_one_embedded_block(CBMExtractCtx *ctx, const CBMEmbeddedBlock *block,
1520-
CBMLanguage embedded_language, bool extract_structure) {
1532+
CBMLanguage embedded_language) {
15211533
const TSLanguage *language = cbm_ts_language(embedded_language);
15221534
if (!language || ctx->source_len < 0) {
15231535
return;
@@ -1564,13 +1576,9 @@ static void parse_one_embedded_block(CBMExtractCtx *ctx, const CBMEmbeddedBlock
15641576
.macro_table = ctx->macro_table,
15651577
.return_type_table = ctx->return_type_table,
15661578
};
1567-
if (extract_structure) {
1568-
cbm_extract_definitions_without_module(&sub_ctx);
1569-
}
1579+
cbm_extract_definitions_without_module(&sub_ctx);
15701580
walk_es_imports(&sub_ctx, sub_ctx.root);
1571-
if (extract_structure) {
1572-
cbm_extract_unified(&sub_ctx);
1573-
}
1581+
cbm_extract_unified(&sub_ctx);
15741582

15751583
ts_tree_delete(tree);
15761584
ts_parser_delete(parser);
@@ -1591,19 +1599,17 @@ static void parse_embedded_imports(CBMExtractCtx *ctx) {
15911599
/* Structure (defs + calls), not just imports, for hosts whose
15921600
* embedded language carries real code. Vue since #1852; CFML's
15931601
* <cfscript> since the #1412 distillation. */
1594-
bool extract_structure =
1595-
ctx->language == CBM_LANG_VUE || ctx->language == CBM_LANG_CFML;
15961602
/* The attribute resolver is Vue's: it inspects <script lang=/src=>
15971603
* and OVERRIDES the spec's embedded language (JS default, TS on
15981604
* lang="ts", bail on src=). CFML's cf_script_tag carries no such
15991605
* attributes and its embedded language is fixed by the spec row
16001606
* (CFSCRIPT) — running the resolver would silently rewrite it to
16011607
* JavaScript. Resolve only for Vue. */
1602-
if (ctx->language == CBM_LANG_VUE &&
1603-
!vue_embedded_language(ctx, hits[i].script, &embedded_language)) {
1608+
if (markup_script_host(ctx->language) &&
1609+
!script_embedded_language(ctx, hits[i].script, &embedded_language)) {
16041610
continue;
16051611
}
1606-
parse_one_embedded_block(ctx, &hits[i], embedded_language, extract_structure);
1612+
parse_one_embedded_block(ctx, &hits[i], embedded_language);
16071613
}
16081614
}
16091615
}

‎internal/cbm/lang_specs.c‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -917,8 +917,8 @@ static const CBMEmbeddedLangSpec astro_embedded_imports[] = {
917917
/* Astro component scripts live in the `---` frontmatter fence, which the
918918
* grammar keeps as an unparsed frontmatter_js_block. Re-parse that slice
919919
* with the JS grammar so `import X from './X.astro'` becomes a real edge. */
920-
{"frontmatter", "frontmatter_js_block", CBM_LANG_JAVASCRIPT},
921-
{"script_element", "raw_text", CBM_LANG_JAVASCRIPT},
920+
{"frontmatter", "frontmatter_js_block", CBM_LANG_TYPESCRIPT},
921+
{"script_element", "raw_text", CBM_LANG_TYPESCRIPT},
922922
{NULL, NULL, 0},
923923
};
924924

‎src/discover/language.c‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ static const ext_entry_t EXT_TABLE[] = {
5050

5151
/* C# */
5252
{".cs", CBM_LANG_CSHARP},
53+
/* Razor components and Pages use the C# grammar for their code blocks;
54+
* surrounding markup is best-effort and may be reported as partial. */
55+
{".razor", CBM_LANG_CSHARP},
56+
{".cshtml", CBM_LANG_CSHARP},
5357

5458
/* Clojure */
5559
{".clj", CBM_LANG_CLOJURE},

‎src/pipeline/pass_complexity.c‎

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,10 @@ static const char *str_or_empty(const char *s) {
113113
return s ? s : "";
114114
}
115115

116+
static int cmp_int(int a, int b) {
117+
return a < b ? TLD_CMP_LESS : a > b ? TLD_CMP_GREATER : 0;
118+
}
119+
116120
static int cmp_node_canonical(const cbm_gbuf_node_t *a, const cbm_gbuf_node_t *b) {
117121
int r = strcmp(str_or_empty(a ? a->qualified_name : NULL),
118122
str_or_empty(b ? b->qualified_name : NULL));
@@ -123,12 +127,19 @@ static int cmp_node_canonical(const cbm_gbuf_node_t *a, const cbm_gbuf_node_t *b
123127
if (r != 0) {
124128
return r;
125129
}
126-
int la = a ? a->start_line : 0;
127-
int lb = b ? b->start_line : 0;
128-
if (la != lb) {
129-
return la < lb ? TLD_CMP_LESS : TLD_CMP_GREATER;
130+
r = cmp_int(a ? a->start_line : 0, b ? b->start_line : 0);
131+
if (r != 0) {
132+
return r;
133+
}
134+
r = cmp_int(a ? a->end_line : 0, b ? b->end_line : 0);
135+
if (r != 0) {
136+
return r;
137+
}
138+
r = strcmp(str_or_empty(a ? a->name : NULL), str_or_empty(b ? b->name : NULL));
139+
if (r != 0) {
140+
return r;
130141
}
131-
return 0;
142+
return strcmp(str_or_empty(a ? a->label : NULL), str_or_empty(b ? b->label : NULL));
132143
}
133144

134145
static int cmp_seed_canonical(const void *pa, const void *pb) {

‎src/pipeline/pass_route_nodes.c‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -480,10 +480,12 @@ static int ensure_one_decorator_route(cbm_gbuf_t *gb, const cbm_gbuf_node_t *fun
480480

481481
/* Phase 2a: Ensure all functions with route_path properties have Route+HANDLES edges. */
482482
static void ensure_decorator_routes(cbm_gbuf_t *gb) {
483-
const char *labels[] = {"Function", "Method"};
483+
/* Razor components/pages carry their route on the implicit Module. */
484+
const char *labels[] = {"Function", "Method", "Module"};
485+
const int label_count = (int)(sizeof(labels) / sizeof(labels[0]));
484486
int created = 0;
485487

486-
for (int li = 0; li < RN_STRIP_PASSES; li++) {
488+
for (int li = 0; li < label_count; li++) {
487489
const cbm_gbuf_node_t **nodes = NULL;
488490
int count = 0;
489491
if (cbm_gbuf_find_by_label(gb, labels[li], &nodes, &count) != 0) {

0 commit comments

Comments
 (0)