scc-context 0.2.11

System Context Compiler context: ranking and bounded context packs
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
//! Startup context (Wave 14C): the deterministic Atlas + Surface fusion
//! handed to agents at session start, plus the task delta (Wave 14E) that
//! renders only *new* relevant APIs against the context ledger.
//!
//! Prompt-cache stability: the artifact hash is a pure function of
//! `(epoch, renderer_version, trust_policy, budget)` โ€” no timestamps โ€” so
//! the same epoch + config always yields byte-identical startup text.

use crate::surface::{build_surface, build_surface_cached, SurfaceMode, SurfacePolicy, SurfaceRequest};
use crate::ContextCompiler;
use scc_core::kinds;
use scc_core::{estimate_tokens, ContextArtifact, ContextBudget, ContextLedger};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};

/// Renderer version: part of the artifact hash. Bump when the startup
/// renderer's output format changes (invalidates prompt-cache keys).
pub const RENDERER_VERSION: &str = env!("CARGO_PKG_VERSION");

/// The startup artifact: atlas + surface + coverage + omissions, with the
/// deterministic artifact hash. `surface_render` is the SAME render the
/// artifact printed โ€” ledger recording derives visible ids from it, so
/// the surface is never computed twice per startup.
// trace:exempt reason=internal-detail
pub struct StartupContext {
    pub atlas: String,
    /// Atlas budget the DELIVERED atlas text was rendered under (normally
    /// the loop's final `atlas_budget`, or the 256-token essentials budget
    /// on the emergency floor). The ledger rebuilds the same pack, so
    /// recorded atlas ids never describe undelivered text (ยง53 coupling).
    pub atlas_budget_used: usize,
    /// Deterministic physical-layout evidence (repository skeleton),
    /// built from the indexed file inventory under its own hard budget.
    pub skeleton: String,
    pub surface: String,
    /// IMPORTANT SYMBOLS section (audit item 3): the fast "where do I pay
    /// attention first" answer, rendered before the long Surface Map
    /// detail. Computed from the same global rank the surface used.
    pub important: String,
    pub surface_render: scc_core::SurfaceRenderResult,
    pub coverage: Vec<String>,
    pub omissions: Vec<String>,
    pub artifact: ContextArtifact,
}
/// Global-rank cache (Wave 15.2, per-ModelEpoch rank caching): the
/// expensive, epoch-stable parts of a global Surface build โ€”
/// `SystemRanker::new` (heterogeneous node graph + adjacency + rarity),
/// the 50-iteration global PageRank vector, and the projection to symbol
/// scores โ€” serialized to the store cache so consecutive startups in the
/// same model epoch skip the rank build entirely.
///
/// Scope evidence (Part F profiling, cli-service fixture, budget 7000):
/// cold rebuild 44-242ยตs vs 25ยตs cache hit โ€” the rebuild is sub-millisecond
/// at fixture scale, so the seam stays STARTUP-ONLY; plain `scc surface` /
/// MCP `surface_map` keep the uncached `build_surface` (identical output,
/// measured cost negligible). Re-measure before extending the seam to
/// larger corpora. Key:
/// `rank:global:<blake3(epoch, policy, salt)[..20]>` (mirrors the
/// `system_atlas` pack-cache pattern in `ContextCompiler::system_atlas`).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
// trace:exempt reason=internal-detail
pub struct GlobalRankCache {
    /// The composite cache epoch the entry was computed under.
    pub epoch: String,
    /// The TrustPolicy fingerprint (`trust_policy_str`) the rank used.
    pub policy: String,
    /// The active rank salt (`ContextSettings::rank_salt`).
    pub salt: String,
    /// The heterogeneous global PageRank vector (index i == `nodes()[i]`).
    /// Retained so a future task-PPR path can warm-start from it.
    pub global_vector: Vec<f64>,
    /// Symbol id -> projected global score (`project_to_symbols` output) โ€”
    /// exactly what the surface pipeline consumes as `global_of`.
    pub node_symbol_map: BTreeMap<String, f64>,
    /// The epoch the candidate entry list came from (epoch-stability
    /// marker; the cache key already pins the epoch).
    pub candidates_epoch: String,
    /// Epoch-stable candidate entry ids (cheap accounting โ€” avoids a
    /// `compile_surface_map` walk for the surface accounting line).
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub candidate_ids: Vec<String>,
    /// How many times this entry has been reused (deterministic
    /// cache-hit marker for tests).
    #[serde(default)]
    pub hits: u64,
}

/// THE one startup-budget allocator (transport parity): every transport
/// that builds a startup artifact โ€” CLI `context startup`, MCP
/// `system_context`, HTTP, Hermes, the SDKs, the Claude SessionStart /
/// PreCompact hooks, and the benchmark harness โ€” resolves its budget
/// through this function and never derives an Atlas:Surface split itself.
///
/// `target_tokens == None` does NOT bypass adaptation: it selects the
/// configured startup ceiling (`compiler.settings.startup_tokens`), which
/// then goes through the SAME adaptive complexity split. An explicit target
/// scales the total; repo complexity decides the split โ€” in both cases via
/// [`scc_core::ContextBudget::adaptive`]. Deterministic per (target,
/// view): same inputs, same budget.
// trace:v1 id=impl.scc.startup.allocate-budget work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching satisfies=REQ-adaptive-startup-budgets
pub fn allocate_startup_budget(
    compiler: &ContextCompiler,
    target_tokens: Option<usize>,
) -> ContextBudget {
    let total = target_tokens.unwrap_or(compiler.settings.startup_tokens);
    let view = &compiler.view;
    let entity_count = view.entities().count();
    let component_count = view.components().len();
    let flow_count = view.flows().len();
    let surface_candidates = view.entities_of_kind(scc_core::kinds::SYMBOL).len();
    scc_core::ContextBudget::adaptive(total, entity_count, component_count, flow_count, surface_candidates)
}

// trace:v1 id=impl.crates-scc-context-src-startup.coverage-lines work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching satisfies=REQ-hard-max-invariant-on-rendered-text
fn coverage_lines(
    compiler: &ContextCompiler,
    render_ids: usize,
    candidates: usize,
    render_tokens: usize,
    surface_budget: usize,
) -> Vec<String> {
    let mut coverage = Vec::new();
    for w in compiler_warnings(compiler) {
        coverage.push(w);
    }
    let mut stale: Vec<String> = compiler.stale_paths.clone();
    stale.sort();
    stale.dedup();
    // Bounded: per-path lines aid small repos; a 10k-stale repo reports
    // counts, never 10k lines (the emergency floor omits paths entirely).
    const MAX_STALE_LINES: usize = 10;
    for p in stale.iter().take(MAX_STALE_LINES) {
        coverage.push(format!("stale: {p}"));
    }
    if stale.len() > MAX_STALE_LINES {
        coverage.push(format!(
            "stale: โ€ฆand {} more changed file(s) not yet re-indexed",
            stale.len() - MAX_STALE_LINES
        ));
    }
    coverage.push(format!(
        "surface map: {} of {} entries rendered, {} tokens (budget {})",
        render_ids,
        candidates,
        render_tokens,
        surface_budget
    ));
    coverage
}

// OMISSIONS helper (module-level so its trace marker attaches): the render
// result's per-kind cuts + omitted-id count + the
// atlas's dropped sections (honest: omitted ids are never silent).
// trace:v1 id=impl.crates-scc-context-src-startup.omission-lines
fn omission_lines(
    render_omissions: &[scc_core::SurfaceOmission],
    omitted_len: usize,
    atlas_dropped: &[String],
    atlas_hard_truncated: bool,
    atlas_exceeded: bool,
) -> Vec<String> {
    let mut omissions = Vec::new();
    for o in render_omissions {
        omissions.push(format!("surface: {} ({})", o.kind, o.reason));
    }
    if omitted_len > 0 {
        omissions.push(format!(
            "surface: {} lower-ranked definitions omitted",
            omitted_len
        ));
    }
    for d in atlas_dropped {
        omissions.push(format!("atlas section dropped: {d}"));
    }
    if atlas_hard_truncated {
        omissions.push("atlas hard-truncated mid-section".into());
    }
    if atlas_exceeded {
        omissions.push("atlas exceeded soft budget: kept complete, over budget".into());
    }
    if omissions.is_empty() {
        omissions.push("none".into());
    }
    omissions
}

/// Build the deterministic startup artifact: the existing System Atlas
/// content (capped at `budget.atlas`) fused with the budget-selected
/// System Surface Map (the production `select_and_render_global` pipeline),
/// coverage warnings, and honest omissions. The surface's omitted ids
/// populate the OMISSIONS section; the ledger records ONLY rendered ids.
// trace:v1 id=impl.scc.context.startup work=WORK-SCC-014 satisfies=REQ-SCC-IR
pub fn build_startup(
    compiler: &ContextCompiler,
    budget: &ContextBudget,
    renderer_version: &str,
) -> StartupContext {
    let epoch = compiler
        .store
        .cache_epoch()
        .unwrap_or_else(|_| "no-epoch".into());

    // Atlas: reuse the existing cached-atlas pipeline (system_atlas is
    // cache-keyed by epoch + stale set; a stale atlas is never served).
    let atlas_pack = compiler.system_atlas(Some(budget.atlas));
    let atlas = atlas_pack.content.clone();

    // Repository Skeleton: physical-layout evidence from the indexed file
    // inventory, under its own hard budget. Pre-bounded (never rebalanced
    // by the corrective loop below), deterministic per epoch, and counted
    // in every fused hard-max probe.
    let paths: Vec<String> = compiler
        .store
        .all_files()
        .unwrap_or_default()
        .into_iter()
        .map(|(p, _, _, _, _)| p)
        .collect();
    let mut skeleton =
        crate::skeleton::build_skeleton(&paths, crate::skeleton::skeleton_budget(budget.total)).text;

    // Surface: the FULL production pipeline โ€” the one authoritative
    // [`build_surface`] service in Global mode (heterogeneous global PPR,
    // required coverage, MMR diversity, token-aware quotas, soft/hard
    // budget selection, render) โ€” built EXACTLY ONCE per startup. The
    // per-ModelEpoch global-rank cache supplies the PPR vector + symbol
    // projection on hit (skipping SystemRanker::new + the 50 power
    // iterations); on miss the render fills the cache, persisted below.
    let mut rank_cache = load_global_rank_cache(compiler);
    let cache_hit = rank_cache.is_some();
    let render = build_surface_cached(
        compiler,
        SurfaceRequest {
            mode: SurfaceMode::Global,
            budget: budget.surface,
            explain: false,
            policy: SurfacePolicy::defaults(budget.surface),
            semantic: None,
        },
        &mut rank_cache,
    );
    // Startup hard-max semantics (Part E): `budget.total` is the SOFT
    // target; the hard maximum is target + 20% (min +500). Atlas and
    // Surface may borrow from each other within it: if the FUSED body
    // (atlas + coverage + omissions + surface) exceeds the hard max, the
    // surface slice is re-selected against whatever room the atlas's
    // ACTUAL size left โ€” never by silently cutting the artifact.
    let startup_hard_max = budget.total.saturating_add((budget.total / 5).max(500));

    let mut render = render;
    let mut atlas_pack = atlas_pack;
    let mut atlas = atlas;
    let mut atlas_budget = budget.atlas;
    // MODEL COVERAGE: compiler warnings + stale paths + a surface accounting
    // line, derived from the render itself (rendered โˆช omitted == every
    // candidate) โ€” never a second compile_surface_map walk. Recomputed
    // after any hard-max rebalance below.
    let mut coverage = coverage_lines(
        compiler,
        render.rendered_ids.len(),
        render.rendered_ids.len() + render.omitted_ids.len(),
        render.token_count,
        budget.surface,
    );

    // Startup hard-max CORRECTIVE LOOP (Part 4): the invariant is on the
    // FINAL assembled text โ€” `estimate_tokens(assemble_body(atlas, surface,
    // coverage, omissions)) <= startup_hard_max`. Headers, coverage lines,
    // omission text, and the atlas's actual size can each push the fused
    // body past the room estimated pre-assembly, so the ONE-SHOT rebalance
    // is not sufficient. This loop reassembles and recounts after every
    // change; first it shrinks the Surface (re-selected with hard_max = the
    // room the current atlas/coverage/omissions actually leave), then, when
    // the Surface is at its floor (no room left), it shrinks the Atlas
    // budget in 25% steps โ€” dropping lower-priority sections that are
    // RECORDED via atlas_pack.dropped_sections and surfaced in OMISSIONS.
    // Deterministic convergence, not a fixed round cap: each round either
    // shrinks the Surface (re-selected with hard_max = the room the current
    // atlas/coverage/omissions actually leave) or, once the surface is at
    // its 64-token floor, shrinks the Atlas budget by 25% (dropping
    // lower-priority sections that are RECORDED via dropped_sections and
    // surfaced in OMISSIONS). Both steps are monotone, so the loop
    // terminates at the natural floor in a bounded number of rounds (well
    // under the 64-round safety net). The fit check counts the FULL
    // assembled block (the "# SCC SYSTEM CONTEXT" header + artifact comment
    // are part of the final text), so the invariant holds on artifact.text.
    const BLOCK_HEADER_OVERHEAD: usize = 40; // header + metadata comment, chars/4
    let mut iterations = 0;
    loop {
        let omissions_probe = omission_lines(
            &render.omissions,
            render.omitted_ids.len(),
            &atlas_pack.dropped_sections,
            atlas_pack.hard_truncated,
            atlas_pack.exceeded_soft_budget,
        );
        let fused = assemble_body(&atlas, &skeleton, &render.text, "", &coverage, &omissions_probe);
        if BLOCK_HEADER_OVERHEAD + estimate_tokens(&fused) <= startup_hard_max {
            break;
        }
        iterations += 1;
        if iterations > 64 {
            // Safety net: the natural floors (surface < 64 tokens + atlas
            // floor) always terminate well before this; if both floors are
            // exhausted and the artifact STILL exceeds the hard max, the
            // final assert below documents the pathological residual rather
            // than looping forever.
            break;
        }
        let overhead = estimate_tokens(&assemble_body(&atlas, &skeleton, "", "", &coverage, &omissions_probe));
        let room = startup_hard_max.saturating_sub(overhead);
        if room >= 64 {
            // Surface still has room: shrink it to the room the current
            // atlas + coverage + omissions ACTUALLY leave (headers included).
            let policy = SurfacePolicy {
                quotas: true,
                mmr: true,
                coverage: true,
                hard_max: room,
            };
            render = build_surface_cached(
                compiler,
                SurfaceRequest {
                    mode: SurfaceMode::Global,
                    budget: room,
                    explain: false,
                    policy,
                    semantic: None,
                },
                &mut rank_cache,
            );
            coverage = coverage_lines(
                compiler,
                render.rendered_ids.len(),
                render.rendered_ids.len() + render.omitted_ids.len(),
                render.token_count,
                room,
            );
        } else {
            // Surface at its floor: shrink the Atlas in 25% steps.
            let smaller = atlas_budget.saturating_mul(3) / 4;
            if smaller == atlas_budget {
                break; // atlas floor reached โ€” nothing more to give.
            }
            atlas_budget = smaller;
            atlas_pack = compiler.system_atlas(Some(atlas_budget));
            atlas = atlas_pack.content.clone();
        }
    }

    // Best-effort persistence AFTER the corrective loop (the final surface
    // render also feeds the cache): a cache failure never fails startup.
    // The hit counter is the deterministic "reused on run 2" marker.
    if let Some(c) = &mut rank_cache {
        if cache_hit {
            c.hits += 1;
        }
        store_global_rank_cache(compiler, c);
    }
    let mut surface = render.text.clone();
    let mut atlas_budget_used = atlas_budget;

    // Emergency floor (Part 4): the corrective loop guarantees fit
    // whenever the Surface/Atlas floors leave room. When even the floors
    // overflow (a tiny budget on a large repository), degrade
    // structurally instead of escaping the cap: atlas essentials at a
    // fixed 256-token budget, surface omitted (ledger ids cleared so ยง53
    // coupling holds), skeleton shrunk until the fused receipt fits. The
    // delivered text ALWAYS satisfies the hard max โ€” there is no overflow
    // escape hatch. `atlas_budget_used` tracks the delivered atlas so the
    // ledger rebuilds the same pack it describes.
    let emergency_omissions: Option<Vec<String>>;
    {
        let omissions_probe0 = omission_lines(
            &render.omissions,
            render.omitted_ids.len(),
            &atlas_pack.dropped_sections,
            atlas_pack.hard_truncated,
            atlas_pack.exceeded_soft_budget,
        );
        if BLOCK_HEADER_OVERHEAD
            + estimate_tokens(&assemble_body(
                &atlas,
                &skeleton,
                &surface,
                "",
                &coverage,
                &omissions_probe0,
            ))
            <= startup_hard_max
        {
            emergency_omissions = None;
        } else {

        atlas_budget_used = 256;
        atlas_pack = compiler.system_atlas(Some(atlas_budget_used));
        atlas = atlas_pack.content.clone();
        render.rendered_ids.clear();
        render.omitted_ids.clear();
        render.omissions.clear();
        render.text = String::new();
        render.token_count = 0;
        surface = String::new();
        // Minimal emergency coverage: warnings + counts, never per-path
        // lines (a 10k-stale repo must still fit a tiny budget).
        coverage = compiler_warnings(compiler);
        if compiler.stale_paths.is_empty() {
            coverage.push("model: current".into());
        } else {
            coverage.push(format!(
                "model stale: {} changed file(s) not yet re-indexed (paths omitted over hard max)",
                compiler.stale_paths.len()
            ));
        }
        coverage.push("surface map: omitted over hard max (0 rendered)".into());
        let mut skel_budget = crate::skeleton::skeleton_budget(budget.total);
        loop {
            let probe = crate::skeleton::build_skeleton(&paths, skel_budget);
            let omissions_probe = {
            let mut o = omission_lines(
                &[],
                0,
                &atlas_pack.dropped_sections,
                atlas_pack.hard_truncated,
                atlas_pack.exceeded_soft_budget,
            );
            o.push(
                "startup emergency compression: atlas essentials + skeleton only (surface omitted over hard max)".into(),
            );
            o };
            let fused = assemble_body(&atlas, &probe.text, "", "", &coverage, &omissions_probe);
            if BLOCK_HEADER_OVERHEAD + estimate_tokens(&fused) <= startup_hard_max
                || skel_budget == 0
            {
                skeleton = probe.text;
                emergency_omissions = Some(omissions_probe);
                break;
            }
            skel_budget /= 2;
        }
    }
    }

    // IMPORTANT FUNDED (audit item 3): the section joins the fused body,
    // so it must fit the hard max like every other section. If the
    // post-loop important text pushes the fused artifact over hard_max,
    // shrink the surface once more by exactly that overrun and recompute
    // the section from the new render (monotone: at most one extra pass).
    // Section budget: ~8% of the hard max at ~40 tokens/symbol,
    // floored to 1 symbol and capped at 15. Tiny totals list fewer
    // symbols instead of either blowing the hard max or flooring to
    // nothing. Receipt: 15 symbols render ~550 tokens; a 1024-total
    // startup (hard max ~1500) funds ~120 section tokens โ‰ˆ 3 symbols.
    let important_n = (startup_hard_max / 500).clamp(1, 15);
    let mut important = if render.rendered_ids.is_empty() {
        "## SYSTEM-CRITICAL SYMBOLS\n(surface omitted over hard max)\n".to_string()
    } else {
        let top = crate::surface::important_symbols(
            compiler,
            crate::surface::SurfaceMode::Global,
            important_n,
        );
        crate::surface::render_important(&top, false)
    };
    {
        let probe_om = omission_lines(
            &render.omissions,
            render.omitted_ids.len(),
            &atlas_pack.dropped_sections,
            atlas_pack.hard_truncated,
            atlas_pack.exceeded_soft_budget,
        );
        let fused = assemble_body(&atlas, &skeleton, &render.text, &important, &coverage, &probe_om);
        if BLOCK_HEADER_OVERHEAD + estimate_tokens(&fused) > startup_hard_max && !render.rendered_ids.is_empty() {
            let over = BLOCK_HEADER_OVERHEAD + estimate_tokens(&fused) - startup_hard_max;
            let room = render
                .token_count
                .saturating_sub(over)
                .saturating_sub(estimate_tokens(&important));
            if room >= 64 {
                let policy = SurfacePolicy {
                    quotas: true,
                    mmr: true,
                    coverage: true,
                    hard_max: room,
                };
                render = build_surface_cached(
                    compiler,
                    SurfaceRequest {
                        mode: SurfaceMode::Global,
                        budget: room,
                        explain: false,
                        policy,
                        semantic: None,
                    },
                    &mut rank_cache,
                );
                coverage = coverage_lines(
                    compiler,
                    render.rendered_ids.len(),
                    render.rendered_ids.len() + render.omitted_ids.len(),
                    render.token_count,
                    room,
                );
                important = if render.rendered_ids.is_empty() {
                    "## SYSTEM-CRITICAL SYMBOLS\n(surface omitted over hard max)\n".to_string()
                } else {
                    let top = crate::surface::important_symbols(
                        compiler,
                        crate::surface::SurfaceMode::Global,
                        important_n,
                    );
                    crate::surface::render_important(&top, false)
                };
                // Final probe: the recomputed section + smaller surface
                // may still exceed (tiny total, huge repo) โ€” floor it.
                let probe_om2 = omission_lines(
                    &render.omissions,
                    render.omitted_ids.len(),
                    &atlas_pack.dropped_sections,
                    atlas_pack.hard_truncated,
                    atlas_pack.exceeded_soft_budget,
                );
                let fused2 = assemble_body(&atlas, &skeleton, &render.text, &important, &coverage, &probe_om2);
                if BLOCK_HEADER_OVERHEAD + estimate_tokens(&fused2) > startup_hard_max {
                    important = "## SYSTEM-CRITICAL SYMBOLS\n(omitted over hard max)\n".to_string();
                }
            }
        } else {
            // No room to fund the section: floor it to one line.
            important = "## SYSTEM-CRITICAL SYMBOLS\n(omitted over hard max)\n".to_string();
        }
    }

    // OMISSIONS (final render โ€” after the corrective loop): the render
    // result's per-kind cuts + omitted-id count + the atlas's dropped
    // sections (honest: omitted ids are never silent).
    let omissions = emergency_omissions.unwrap_or_else(|| {
        omission_lines(
            &render.omissions,
            render.omitted_ids.len(),
            &atlas_pack.dropped_sections,
            atlas_pack.hard_truncated,
            atlas_pack.exceeded_soft_budget,
        )
    });

    let trust_policy = trust_policy_str(compiler.view.policy());

    // Deterministic artifact hash: blake3 over (epoch + renderer_version +
    // trust_policy + budget fields). The preimage never contains
    // timestamps or volatile state, so the hash is stable per epoch.
    let mut h = blake3::Hasher::new();
    h.update(b"startup-artifact-v1");
    h.update(epoch.as_bytes());
    h.update(renderer_version.as_bytes());
    h.update(trust_policy.as_bytes());
    h.update(budget.total.to_string().as_bytes());
    h.update(budget.atlas.to_string().as_bytes());
    h.update(budget.surface.to_string().as_bytes());
    h.update(budget.task_delta.to_string().as_bytes());
    h.update(budget.structural_source.to_string().as_bytes());
    let sha256 = h.finalize().to_hex().to_string();

    // CONTENT hash: the same config preimage PLUS the actual rendered text
    // (atlas + surface + coverage + omissions, without the artifact metadata
    // comment). A content change that keeps the config identical now
    // changes the hash โ€” the audit's name/content mismatch fix.
    let body = assemble_body(&atlas, &skeleton, &surface, &important, &coverage, &omissions);
    let mut ch = blake3::Hasher::new();
    ch.update(b"startup-content-v1");
    ch.update(epoch.as_bytes());
    ch.update(renderer_version.as_bytes());
    ch.update(trust_policy.as_bytes());
    ch.update(budget.total.to_string().as_bytes());
    ch.update(budget.atlas.to_string().as_bytes());
    ch.update(budget.surface.to_string().as_bytes());
    ch.update(budget.task_delta.to_string().as_bytes());
    ch.update(budget.structural_source.to_string().as_bytes());
    ch.update(body.as_bytes());
    let content_hash = ch.finalize().to_hex().to_string();

    let mut artifact = ContextArtifact {
        kind: "startup".into(),
        epoch,
        renderer_version: renderer_version.to_string(),
        trust_policy,
        budget: budget.clone(),
        sha256,
        content_hash,
        text: String::new(),
    };
    artifact.text = assemble_block(&atlas, &skeleton, &surface, &important, &coverage, &omissions, &artifact);

    StartupContext {
        atlas,
        atlas_budget_used,
        skeleton,
        surface,
        important,
        surface_render: render,
        coverage,
        omissions,
        artifact,
    }
}
/// The store-cache key for the global rank cache: `rank:global:` +
/// blake3 over the composite cache epoch, the TrustPolicy fingerprint,
/// and the rank salt (truncated to 20 hex chars, mirroring the
/// `system_atlas` pack-cache key pattern). A changed epoch, policy, or
/// salt yields a different key โ€” a stale entry is never served.
// trace:exempt reason=internal-detail
fn global_rank_key(epoch: &str, policy: &str, salt: &str, pipeline: &str) -> String {
    let mut h = blake3::Hasher::new();
    h.update(b"rank:global:v1");
    h.update(epoch.as_bytes());
    h.update(b"\0");
    h.update(policy.as_bytes());
    h.update(b"\0");
    h.update(salt.as_bytes());
    h.update(b"\0");
    h.update(pipeline.as_bytes());
    format!("rank:global:{}", &h.finalize().to_hex()[..20])
}


/// Load the per-ModelEpoch global rank cache from the store cache
/// (key `rank:global:<hash>` over epoch + policy + salt). `None` on any
/// miss/error โ€” never panics, never fabricates. The entry is validated
/// against the current epoch/policy/salt (belt-and-suspenders: the key
/// already pins them).
/// Pipeline-aware variant (spec ยง58): `pipeline` is the ranking-pipeline
/// hash (e.g. the engine plugin cache-key fragment). "" preserves the
/// legacy key exactly โ€” no-plugin callers see byte-identical keys.
/// Supersedes impl.scc.startup.rank-cache-load (same body + pipeline).
// trace:v1 id=impl.scc.startup.rank-cache-pipeline work=WORK-SI-MMMJA4G6 satisfies=REQ-SI-503JSBGP
pub fn load_global_rank_cache_with_pipeline(compiler: &ContextCompiler, pipeline: &str) -> Option<GlobalRankCache> {
    let epoch = compiler.store.cache_epoch().ok()?;
    let policy = trust_policy_str(compiler.view.policy());
    let salt = &compiler.settings.rank_salt;
    let key = global_rank_key(&epoch, &policy, salt, pipeline);
    let cached = compiler.store.cache_get(&key, &epoch).ok().flatten()?;
    let c: GlobalRankCache = serde_json::from_str(&cached).ok()?;
    if c.epoch != epoch || c.policy != policy || c.salt != *salt {
        return None;
    }
    Some(c)
}

// trace:exempt reason=internal-detail
pub fn load_global_rank_cache(compiler: &ContextCompiler) -> Option<GlobalRankCache> {
    load_global_rank_cache_with_pipeline(compiler, "")
}

/// Persist the per-ModelEpoch global rank cache (best-effort: cache
/// failures never fail the caller).
// trace:v1 id=impl.scc.startup.rank-cache-store work=WORK-wave-15-2-heterogeneous-hierarchy-edges-semantic-scoring-explain-rank-caching satisfies=REQ-global-rank-cached-per-model-epoch
// trace:exempt reason=internal-detail
pub fn store_global_rank_cache_with_pipeline(compiler: &ContextCompiler, cache: &GlobalRankCache, pipeline: &str) {
    let epoch = compiler
        .store
        .cache_epoch()
        .unwrap_or_else(|_| "no-epoch".into());
    let policy = trust_policy_str(compiler.view.policy());
    let salt = &compiler.settings.rank_salt;
    let key = global_rank_key(&epoch, &policy, salt, pipeline);
    if let Ok(json) = serde_json::to_string(cache) {
        let _ = compiler.store.cache_put(&key, &json, &epoch);
    }
}

// trace:exempt reason=internal-detail
pub fn store_global_rank_cache(compiler: &ContextCompiler, cache: &GlobalRankCache) {
    store_global_rank_cache_with_pipeline(compiler, cache, "")
}

/// The logical symbol entity id of a rendered entry id: overload-sensitive
/// entry ids carry a `#overload{N}` suffix (`{symbol}#overload{N}`) that
/// is stripped to recover the symbol id. Non-overload entry ids ARE the
/// symbol entity id.
// trace:exempt reason=internal-detail
fn symbol_id_of(entry_id: &str) -> String {
    entry_id
        .rsplit_once("#overload")
        .map(|(logical, _)| logical)
        .unwrap_or(entry_id)
        .to_string()
}

/// The spec's startup block format. Pure function of the context struct, so
/// `build_startup(..).artifact.text == render_startup(&startup)` always.
// trace:exempt reason=internal-detail
pub fn render_startup(s: &StartupContext) -> String {
    assemble_block(&s.atlas, &s.skeleton, &s.surface, &s.important, &s.coverage, &s.omissions, &s.artifact)
}

/// The startup body (all content sections, no artifact metadata comment) โ€”
/// the preimage of `content_hash`.
// trace:exempt reason=internal-detail
fn assemble_body(atlas: &str, skeleton: &str, surface: &str, important: &str, coverage: &[String], omissions: &[String]) -> String {
    let mut out = String::new();
    out.push_str("## HOW TO READ THIS PACK\n");
    out.push_str("Machine-generated system context; work within it, do not re-derive.\n");
    out.push_str("ATLAS=architecture, SKELETON=file layout, SURFACE=ranked APIs, SYMBOLS=top attention, COVERAGE=warnings, OMISSIONS=budget cuts.\n");
    out.push_str("Authority: source/runtime > pack > checkpoint > hindsight > assumption. DOCUMENTATION labels are unverified. Verify: `scc verify`; stale: `scc index`.\n");
    out.push_str("Next: `scc context task <goal>`, `scc impact <files>`, `scc drift`, `scc ci check`.\n");
    out.push_str("\n## SYSTEM ATLAS\n");
    out.push_str(atlas.trim_end());
    out.push_str("\n\n## REPOSITORY SKELETON\n");
    out.push_str(skeleton.trim_end());
    out.push_str("\n\n## SYSTEM SURFACE MAP\n");
    out.push_str(surface.trim_end());
    out.push_str("\n\n");
    out.push_str(important.trim_end());
    out.push_str("\n\n## MODEL COVERAGE\n");
    if coverage.is_empty() {
        out.push_str("(no warnings)\n");
    } else {
        for c in coverage {
            out.push_str(c);
            out.push('\n');
        }
    }
    out.push_str("\n## OMISSIONS\n");
    for o in omissions {
        out.push_str(o);
        out.push('\n');
    }
    out
}

// trace:exempt reason=internal-detail
fn assemble_block(
    atlas: &str,
    skeleton: &str,
    surface: &str,
    important: &str,
    coverage: &[String],
    omissions: &[String],
    artifact: &ContextArtifact,
) -> String {
    let mut out = String::new();
    out.push_str("# SCC SYSTEM CONTEXT\n");
    out.push_str(&format!(
        "<!-- artifact sha256:{} content_hash:{} epoch:{} renderer:{} -->\n\n",
        artifact.sha256, artifact.content_hash, artifact.epoch, artifact.renderer_version
    ));
    out.push_str(&assemble_body(atlas, skeleton, surface, important, coverage, omissions));
    out
}

/// Coverage warnings mirroring the compiler's pack warnings (deterministic):
/// not-indexed, stale count, high/critical drift.
// trace:exempt reason=internal-detail
fn compiler_warnings(compiler: &ContextCompiler) -> Vec<String> {
    let mut w = Vec::new();
    if compiler.store.snapshot_status().ok().flatten().is_none() {
        w.push("Repository is not indexed โ€” run `scc index`.".into());
    }
    if !compiler.stale_paths.is_empty() {
        w.push(format!(
            "Model is stale: {} changed file(s) not yet re-indexed.",
            compiler.stale_paths.len()
        ));
    }
    if let Ok(findings) = compiler.store.drift_findings(true) {
        for (_, kind, sev, msg, _) in findings {
            if sev == "high" || sev == "critical" {
                w.push(format!("Drift [{kind}]: {msg}"));
            }
        }
    }
    w.truncate(6);
    w
}

// trace:exempt reason=internal-detail
// trace:exempt reason=internal-detail
pub fn trust_policy_str(p: &scc_graph::TrustPolicy) -> String {
    format!(
        "extracted={} resolved={} observed={} declared={} inferred={} floor={}",
        p.allow_extracted,
        p.allow_resolved,
        p.allow_observed,
        p.allow_declared,
        p.allow_inferred,
        p.min_inferred_confidence
    )
}

/// The kind-scoped id sets shown by the startup artifact (for ledger
/// recording): `(symbols, files, components, flows)`. The surface side
/// records ONLY the render result's `rendered_ids` โ€” budget-omitted
/// candidates are never marked visible (audit fix: the ledger must
/// describe what the agent actually saw). Consumes the ALREADY-PRODUCED
/// render (`startup.surface_render`) โ€” the surface is built exactly once
/// per startup; this function never rebuilds it.
// trace:exempt reason=internal-detail
pub fn visible_ids_from_startup(
    compiler: &ContextCompiler,
    startup: &StartupContext,
) -> (BTreeSet<String>, BTreeSet<String>, BTreeSet<String>, BTreeSet<String>) {
    let mut symbols = BTreeSet::new();
    let mut files = BTreeSet::new();
    let mut components = BTreeSet::new();
    let mut flows = BTreeSet::new();

    // Cache-hit in the CLI flow (build_startup already ran system_atlas
    // at this budget). The DELIVERED budget โ€” not the requested one โ€” so
    // emergency-floor artifacts record only delivered atlas ids (ยง53).
    let atlas_pack = compiler.system_atlas(Some(startup.atlas_budget_used));

    // Surface: rendered entries ONLY โ€” the SAME render the artifact
    // printed, so the ledger exactly matches the artifact text. Entry
    // metadata (symbol id, file path, component) is derived from the
    // trusted view per rendered id (cheap: rendered ids only, never the
    // candidate pool) โ€” no compile_surface_map walk.
    let view = &compiler.view;
    let rendered_symbols: BTreeSet<String> = startup
        .surface_render
        .rendered_ids
        .iter()
        .map(|id| symbol_id_of(id))
        .collect();
    // Component attribution mirrors compile_surface_map's containment
    // walk (component CONTAINS file CONTAINS symbol; first component in
    // name order wins) but only for the rendered symbol ids.
    let mut comp_of: BTreeMap<String, String> = BTreeMap::new();
    for c in view.components() {
        for r in sorted_rels(view.out_pred(&c.id, scc_core::predicates::CONTAINS)) {
            for sr in sorted_rels(view.out_pred(&r.object, scc_core::predicates::CONTAINS)) {
                if rendered_symbols.contains(&sr.object) {
                    comp_of
                        .entry(sr.object.clone())
                        .or_insert_with(|| c.name.clone());
                }
            }
        }
    }
    for id in &startup.surface_render.rendered_ids {
        let symbol_id = symbol_id_of(id);
        symbols.insert(symbol_id.clone());
        // The entry id is the entity id for non-overload entries; overload
        // entries (n >= 1) resolve through the logical symbol id.
        let ent = view.entity(id).or_else(|| view.entity(&symbol_id));
        if let Some(e) = ent {
            if let Some(f) = e.attributes.get("file").and_then(|v| v.as_str()) {
                files.insert(f.to_string());
            }
        }
        if let Some(c) = comp_of.get(&symbol_id) {
            components.insert(c.clone());
        }
    }
    // Classify the atlas pack's entity ids by kind (deterministic).
    for id in &atlas_pack.entity_ids {
        if let Some(e) = compiler.view.entity(id) {
            match e.kind.as_str() {
                kinds::SYMBOL => {
                    symbols.insert(id.clone());
                }
                kinds::FILE => {
                    files.insert(e.name.clone());
                }
                kinds::COMPONENT => {
                    components.insert(id.clone());
                }
                kinds::FLOW => {
                    flows.insert(id.clone());
                }
                _ => {}
            }
        }
    }
    (symbols, files, components, flows)
}

/// Deterministic relationship sort (id, subject, object) โ€” mirrors the
/// surface compiler's traversal order so attribution walks are stable.
// trace:exempt reason=internal-detail
fn sorted_rels(rels: Vec<&scc_core::Relationship>) -> Vec<&scc_core::Relationship> {
    let mut v = rels;
    v.sort_by(|a, b| {
        a.id.cmp(&b.id)
            .then_with(|| a.subject.cmp(&b.subject))
            .then_with(|| a.object.cmp(&b.object))
    });
    v
}

/// `task_delta` plus the ids it rendered (for ledger recording). Routed
/// through the one authoritative [`build_surface`] service in Task mode;
/// never re-dumps the Atlas โ€” the custom task/PPR selection implementation
/// was deleted, [`build_surface`] is the only ranking pipeline. `semantic`
/// is the caller-resolved scorer โ€” transport parity: every transport with
/// inference enabled passes the SAME scorer it passed to the task pack
/// (interface choice must not change ranking quality). `None` redistributes
/// the 10% share explicitly.
// trace:v1 id=impl.scc.startup.task-delta-with-ids work=WORK-SCC-014 satisfies=REQ-SCC-IR
pub fn task_delta_with_ids(
    compiler: &ContextCompiler,
    goal: &str,
    visible: &ContextLedger,
    budget_tokens: usize,
    semantic: Option<&dyn crate::rank::SemanticScorer>,
) -> (String, Vec<String>) {
    let render = build_surface(
        compiler,
        SurfaceRequest {
            mode: SurfaceMode::Task {
                goal,
                visible: Some(visible),
            },
            budget: budget_tokens,
            explain: false,
            policy: SurfacePolicy::defaults(budget_tokens),
            semantic,
        },
    );
    // TASK-CRITICAL SYMBOLS (audit item 3): top-8 task-ranked entries
    // with badges + exact counts, ahead of the delta detail.
    let critical = crate::surface::important_symbols(
        compiler,
        SurfaceMode::Task { goal, visible: Some(visible) },
        8,
    );
    let mut out = String::new();
    out.push_str("# SCC TASK DELTA\n");
    out.push_str(&format!("TASK-FOCUS: {goal}\n"));
    out.push_str(crate::surface::render_important(&critical, true).as_str());
    out.push_str("Relevant APIs not already visible:\n");
    let body = render
        .text
        .strip_prefix("SCC SYSTEM SURFACE MAP\n\n")
        .unwrap_or(&render.text);
    out.push_str(body.trim_end());
    out.push('\n');
    (out, render.rendered_ids)
}

/// Task-personalized surface map (full map, no novelty filter โ€” this is a
/// map, not a delta): entries re-ranked by task PPR + importance via the
/// one authoritative [`build_surface`] service in Task mode.
// trace:exempt reason=internal-detail
pub fn task_surface(
    compiler: &ContextCompiler,
    goal: &str,
    budget_tokens: usize,
    explain: bool,
    semantic: Option<&dyn crate::rank::SemanticScorer>,
) -> String {
    task_surface_with_ids(compiler, goal, budget_tokens, explain, semantic).0
}

/// `task_surface` plus the rendered entry ids (for ledger recording).
/// `semantic` is the caller-resolved scorer โ€” transport parity, same rule
/// as [`task_delta_with_ids`].
// trace:v1 id=impl.scc.startup.task-surface-with-ids work=WORK-SCC-014 satisfies=REQ-SCC-IR
pub fn task_surface_with_ids(
    compiler: &ContextCompiler,
    goal: &str,
    budget_tokens: usize,
    explain: bool,
    semantic: Option<&dyn crate::rank::SemanticScorer>,
) -> (String, Vec<String>) {
    let render = build_surface(
        compiler,
        SurfaceRequest {
            mode: SurfaceMode::Task {
                goal,
                visible: None,
            },
            budget: budget_tokens,
            explain,
            policy: SurfacePolicy::defaults(budget_tokens),
            semantic,
        },
    );
    let mut out = String::new();
    out.push_str(&format!("# SYSTEM SURFACE MAP (task-personalized: {goal})\n"));
    let body = render
        .text
        .strip_prefix("SCC SYSTEM SURFACE MAP\n\n")
        .unwrap_or(&render.text);
    out.push_str(body.trim_end());
    out.push('\n');
    (out, render.rendered_ids)
}

#[cfg(test)]
mod tests {
    use super::*;

// trace:exempt reason=internal-detail
    fn fixture_compiler() -> (tempfile::TempDir, crate::ContextCompiler<'static>) {
        let dir = tempfile::TempDir::new().unwrap();
        let root = dir.path().join("repo");
        std::fs::create_dir_all(&root).unwrap();
        let store = Box::leak(Box::new(
            scc_store::Store::open(&dir.path().join("scc.db"), &root).unwrap(),
        ));
        let graph = Box::leak(Box::new(scc_graph::RealityGraph::load(store).unwrap()));
        let settings = crate::ContextSettings::default();
        let comp = crate::ContextCompiler::new(store, graph, settings, Vec::new());
        (dir, comp)
    }

    #[test]
// trace:exempt reason=internal-detail
    fn render_startup_emits_spec_headers() {
        let sc = StartupContext {
            atlas: "ATLAS-BODY".into(),
            atlas_budget_used: 6000,
            skeleton: "SKELETON-BODY".into(),
            surface: "SURFACE-BODY".into(),
            important: "IMPORTANT-BODY".into(),
            surface_render: scc_core::SurfaceRenderResult {
                text: "SURFACE-BODY".into(),
                rendered_ids: vec![],
                rendered_entries: vec![],
                omitted_ids: vec![],
                omissions: vec![],
                token_count: 0,
                critical_drops: vec![],
            },
            coverage: vec!["stale: a.py".into()],
            omissions: vec!["none".into()],
            artifact: ContextArtifact {
                kind: "startup".into(),
                epoch: "epoch:test".into(),
                renderer_version: "test".into(),
                trust_policy: "floor=0.85".into(),
                budget: ContextBudget::default(),
                sha256: "abc".into(),
                content_hash: "def".into(),
                text: String::new(),
            },
        };
        let out = render_startup(&sc);
        assert!(out.contains("# SCC SYSTEM CONTEXT"));
        assert!(out.contains("## SYSTEM ATLAS"));
        assert!(out.contains("## REPOSITORY SKELETON"));
        assert!(out.contains("## SYSTEM SURFACE MAP"));
        assert!(out.contains("ATLAS-BODY"));
        assert!(out.contains("SURFACE-BODY"));
        assert!(out.contains("SKELETON-BODY"));
        assert!(out.contains("## SYSTEM-CRITICAL SYMBOLS") || out.contains("IMPORTANT-BODY"));
        let (ia, ik, is) = (
            out.find("## SYSTEM ATLAS").unwrap(),
            out.find("## REPOSITORY SKELETON").unwrap(),
            out.find("## SYSTEM SURFACE MAP").unwrap(),
        );
        assert!(ia < ik && ik < is, "skeleton grounds before architecture");
        assert!(out.contains("stale: a.py"));
    }

    #[test]
// trace:exempt reason=internal-detail
    fn artifact_text_equals_rendered_block() {
        let (_dir, comp) = fixture_compiler();
        let budget = ContextBudget::default();
        let sc = build_startup(&comp, &budget, "test-renderer");
        assert_eq!(sc.artifact.text, render_startup(&sc));
        assert_eq!(sc.artifact.sha256.len(), 64);
        assert_eq!(sc.artifact.content_hash.len(), 64);
        assert_ne!(sc.artifact.content_hash, sc.artifact.sha256);
        assert!(render_startup(&sc).contains("content_hash:"));
        assert_eq!(sc.artifact.epoch, comp.store.cache_epoch().unwrap_or_default());
    }

    #[test]
// trace:exempt reason=internal-detail
    fn global_rank_cache_roundtrips_through_the_store() {
        let (_dir, comp) = fixture_compiler();
        // miss on a cold store
        assert!(load_global_rank_cache(&comp).is_none());
        let cache = GlobalRankCache {
            epoch: comp.store.cache_epoch().unwrap_or_else(|_| "no-epoch".into()),
            policy: trust_policy_str(comp.view.policy()),
            salt: comp.settings.rank_salt.clone(),
            global_vector: vec![0.1, 0.2, 0.3],
            node_symbol_map: BTreeMap::from([("repo://r/symbol/a.py/serve".into(), 0.42)]),
            candidates_epoch: comp.store.cache_epoch().unwrap_or_default(),
            candidate_ids: vec!["repo://r/symbol/a.py/serve".into()],
            hits: 1,
        };
        store_global_rank_cache(&comp, &cache);
        let loaded = load_global_rank_cache(&comp).expect("cache entry present after store");
        assert_eq!(loaded, cache);
        assert_eq!(loaded.hits, 1);
        assert_eq!(
            loaded.node_symbol_map.get("repo://r/symbol/a.py/serve"),
            Some(&0.42)
        );
        assert_eq!(loaded.candidates_epoch, loaded.epoch);
    }

    #[test]
// trace:exempt reason=internal-detail
    fn visible_ids_consume_the_same_render_the_artifact_printed() {
        // The ledger-visible surface ids MUST be exactly the render's
        // rendered_ids (never a rebuild, never the candidate pool): every
        // rendered entry's logical symbol is visible. The inverse
        // direction (omitted candidates never visible) is asserted in the
        // indexed CLI fixture (surface_startup.rs) where the atlas symbol
        // set is controlled.
        let (_dir, comp) = fixture_compiler();
        let budget = ContextBudget::default();
        let sc = build_startup(&comp, &budget, "test-renderer");
        assert!(sc.artifact.text.contains(&sc.surface));
        let (syms, _files, _comps, _flows) = visible_ids_from_startup(&comp, &sc);
        for id in &sc.surface_render.rendered_ids {
            let symbol_id = symbol_id_of(id);
            assert!(
                syms.contains(&symbol_id),
                "rendered entry {id} must be marked visible (symbol {symbol_id})"
            );
        }
    }
}