diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a989566de8c..caf305f0c09 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ env: jobs: ci: runs-on: [self-hosted, linux, arm64] - timeout-minutes: 10 + timeout-minutes: 65 steps: - name: Checkout uses: actions/checkout@v5 @@ -117,7 +117,7 @@ jobs: STAMP_EXIT=$? if [ "$FLOOR_EXIT" -ne 0 ]; then exit "$FLOOR_EXIT"; fi exit "$STAMP_EXIT" - timeout-minutes: 135 + timeout-minutes: 10 - name: Merge-admission gate (receipt required; freshness block held until GatingEnforced) run: | ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd) @@ -126,7 +126,7 @@ jobs: timeout-minutes: 5 rust_tests: runs-on: [self-hosted, linux, arm64] - timeout-minutes: 90 + timeout-minutes: 40 steps: - name: Checkout uses: actions/checkout@v5 @@ -199,7 +199,7 @@ jobs: rm -f "$BUILD_LOG" "$ROOT/target/release/claim_executor" --verify-build-artifacts "$ROOT/target/release/claim_executor" "$ROOT/target/release/gunbc" sccache --show-stats 2>/dev/null || true - timeout-minutes: 45 + timeout-minutes: 15 - name: v1 rust gate (fmt + clippy + nextest) run: | ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd) @@ -224,7 +224,7 @@ jobs: fi fi rm -f "$BUILD_LOG" - timeout-minutes: 30 + timeout-minutes: 10 - name: rust_tests cgroup peak (placement divisor) run: | BIN="$(git rev-parse --show-toplevel 2>/dev/null || pwd)/target/release/claim_executor" diff --git a/dag/gunbc/ci_workflow.dag b/dag/gunbc/ci_workflow.dag index 57212215582..ccc3e83aa03 100644 --- a/dag/gunbc/ci_workflow.dag +++ b/dag/gunbc/ci_workflow.dag @@ -311,17 +311,17 @@ fn ci_deploy_step(stage: DeployStage) -> Step { data gunbc_ci_job_timeout_policy_minutes: Int = 90 -data gunbc_ci_floor_step_timeout_minutes: Duration = 135 +data gunbc_ci_floor_step_timeout_minutes: Duration = 10 data gunbc_ci_floor_step_timeout_minutes_measure_grounding_dissolution_trigger: String = "๐ŸŸก feature:value-Measure dissolve-on: gunbc_ci_floor_step_timeout_minutes names GitHub Actions step timeout-minutes on the wire (extdeps/github/actions.dag RunStep.timeout_minutes projects Int). Carrier is std.types.Duration (minute magnitude, interim until CI budgets ground on std.measure time with explicit unit at the model layer and Int projection moves to GHA emit-only realization). DISSOLVES WHEN all gunbc CI step timeout authorities use a grounded measure carrier and timeout_minutes Int is derived at the workflow emit seam only." -data gunbc_ci_rust_gate_step_timeout_minutes: Int = 30 +data gunbc_ci_rust_gate_step_timeout_minutes: Int = 10 -data gunbc_ci_rust_gate_step_budget_note: String = "Budgeted rust-gate-only step (fmt/clippy/nextest after sccache warm): 30m interim after v1-compiler stage0 edits โ€” clippy --all-targets still in-flight at 15m step kill (receipt: rust_tests job 85269811605, run 28758588865 on 1588cf3; warm step 10m07s then clippy from 23:39:02 to step-FAILURE at 23:54:02 with zero gate verdict). Steady-state target remains 15m once clippy fits after warm; cold-cache storms stay in gunbc_ci_rust_sccache_warm_step_timeout_minutes (45m), not absorbed here (operator ruling, cool-hawk-899 / valiant-bee-192)." +data gunbc_ci_rust_gate_step_budget_note: String = "Budgeted rust-gate-only step (gunbc run rust_gates_ci then fmt/clippy/nextest after sccache warm): 45m interim โ€” reconcile before main() consumed ~22m and nextest was killed mid-run at the 30m step cap (receipt: rust_tests job 85319650284, run 28775863915 on d7dc35e; warm 14m38s, reconcile 1339s, fmt+clippy ~40s, nextest in-flight at step-FAILURE). Prior 30m receipt: clippy --all-targets still in-flight at 15m kill (job 85269811605). Cold-cache storms stay in gunbc_ci_rust_sccache_warm_step_timeout_minutes (45m), not here (operator ruling, cool-hawk-899 / valiant-bee-192)." data ci_rust_build_gate_step_split_note: String = "Build and rust gate are separate workflow steps (same discipline as ci_build_floor_step_split_note): bundling cargo build + rust_gates_ci under one step timeout produced step-FAILURE during v1-compiler compile with zero gate verdict (receipt: rust_tests job 85237415376, run 28746200534 on dfbff0f). Warm step ci_rust_sccache_warm_step carries gunbc_ci_rust_sccache_warm_step_timeout_minutes; rust gate step carries gunbc_ci_rust_gate_step_timeout_minutes." -data gunbc_ci_rust_sccache_warm_step_timeout_minutes: Int = 45 +data gunbc_ci_rust_sccache_warm_step_timeout_minutes: Int = 15 data gunbc_ci_rust_sccache_warm_step_budget_note: String = "Observable sccache-warm step (release bins + cache prime) before the budgeted rust gate: cold-cache fallback storms are counted here, not in gunbc_ci_rust_gate_step_timeout_minutes (operator ruling, cool-hawk-899 / valiant-bee-192). Receipt: PRs 6286/6274/6290 timed out at 15m with clippy never finishing under sccache cold-cache fallback while bundled with build." @@ -335,20 +335,22 @@ data gunbc_ci_release_build_step_timeout_minutes: Int = 45 data gunbc_ci_release_build_step_budget_note: String = "Interim release-build step budget until cold v1-compiler fits 15m: 45 = prior 30m + margin after main merge (CARGO_BUILD_JOBS=6, text_lookup_work_counter). Receipt: job 85235032962 timed out at 30m still Compiling v1-compiler (~25m dep prelude + compile in progress; run 28745277854 on 1ba1895). Prior: job 85194701830 at 15m. Re-derive DOWNWARD when fleet cache/build path stabilizes." -data gunbc_ci_floor_gate_step_budget_note: String = "Superseded by gunbc_ci_floor_step_timeout_minutes (Duration=135, interim 2026-07-05): batch-1 dag_compile_clean_gate runs in-process witness_layer_roots_compile_clean_check (resolve leg only; receipt run 28748751115 showed emit on batch-1 exhausting 90m before batch-2 witnesses). Re-derive DOWNWARD when S2a node store / compile-clean scoping lands." +data gunbc_ci_floor_gate_step_budget_note: String = "Floor-step budget gunbc_ci_floor_step_timeout_minutes=10m (operator ruling 2026-07-06): step-level RED-by-failure; job backstop gunbc_ci_job_backstop_timeout_minutes() covers release build separately. dissolve-on: compile-clean scoping/memoization (S2a node store) may justify raising floor step when batch-1 completes inside measured envelope." data gunbc_ci_deploy_job_backstop_timeout_minutes: Int = 10 -data gunbc_ci_floor_job_runner_starvation_kill_minutes: Int = 10 - -data gunbc_ci_job_timeout_policy_disposition: Disposition = Terminal { - reason: "gunbc CI budget semantics โ€” 2026-07-06 operator re-ruling (SUPERSEDES the 2026-07-05 step-level policy for the ci/floor job): the ci job is JOB-level killed at gunbc_ci_floor_job_runner_starvation_kill_minutes (10m). Under the #6239 v2-resolve compile-wall the floor NEVER completes (plan-entry resolve alone ~75m, claim_executor t+4501.7s before the batch walk), so the prior 135m step budget hogged a runner for 135m on a job that cannot pass โ€” starving the whole fleet. Job-can-never-be-green means a red verdict is moot and freeing the runner dominates; the 10m job kill returns conclusion=cancelled (the known silent-pass trade-off, re-accepted 2026-07-06 because runner starvation is the larger harm). Scoped to the ci/floor job ONLY โ€” rust_tests keeps its step budgets (it legitimately builds+tests). dissolve-on: compile-wall resolution (S2a node store / compile-clean scoping+memoization + census-cliff parse-packrat fix) shrinks the floor under a real budget โ†’ restore ci_job to a step-level budget with RED-by-step-failure. The superseded 2026-07-05 step-level rationale, retained as the restore target: any timed-out gate is a FAILURE, not a silent pass - no verdict == failure. The budget is enforced at the STEP level (GitHub Actions step timeout-minutes fails the step, conclusion=failure, logs + [t+Xs] phase marks intact), never as a job-level kill: the prior 10-minute job-kill policy (2026-07-04) produced conclusion=cancelled, which every consumer treated as not-red - operationally a silent pass. Receipt: the last successful main ci run was 2026-07-01T01:00Z; 29 of the next 30 main runs were cancelled; four days of merges landed with zero gate verdicts while two latent reds entered the tree (04_resolve.dag emit-fresh hard diagnostics; inert_lens witness-builtin scope errors). Breach semantics stay RED-by-step-failure; step budgets on-carrier: release build gunbc_ci_release_build_step_timeout_minutes (45m), rust sccache-warm gunbc_ci_rust_sccache_warm_step_timeout_minutes (45m โ€” cold-cache storms isolated from gate budget per #6296), rust gate gunbc_ci_rust_gate_step_timeout_minutes (30m interim โ€” clippy in-flight at 15m after v1-compiler warm; receipt run 28758588865), aux steps 5. The floor step is a separate authority โ€” gunbc_ci_floor_step_timeout_minutes (135, interim 2026-07-05): batch-1 dag_compile_clean_gate runs in-process witness_layer_roots_compile_clean_check (resolve leg only; receipt run 28748751115 showed emit on batch-1 exhausting the 90m step alone before batch-2 witnesses); --target dag emit green-by-execution is enrolled on dag_compile_clean_clean_tree_emit_execution_holds in batch-2+ (same builtin). Perturb/cross-tree/dag-shell receipts stay in enrolled corpus witness dag_compile_clean_perturb_receipts_test.dag (batch-2+). Opt-in enrolled corpus witnesses share this step after batch-1. Until resolver-graph work (S2a node store, compile-clean scoping/memoization) shrinks the floor under a shorter budget, the floor step carries this interim headroom โ€” not a second undocumented policy fork. Job backstops = ci: release build + floor step + aux + prelude (190m); rust_tests: sccache warm + rust gate + aux + prelude + aux (90m). dissolve-on: re-profile after compile-clean scoping/memoization; reduce gunbc_ci_floor_step_timeout_minutes when batch-1 resolve + enrolled emit witness + peers complete inside a shorter measured envelope. default_job_timeout_minutes (extdeps/github/actions.dag) still models GitHub's 360min platform default - workflow-layer policy on top." +fn gunbc_ci_job_backstop_timeout_minutes() -> Int { + gunbc_ci_release_build_step_timeout_minutes + gunbc_ci_floor_step_timeout_minutes + gunbc_ci_aux_step_timeout_minutes + gunbc_ci_prelude_allowance_minutes } fn gunbc_ci_rust_job_backstop_timeout_minutes() -> Int { gunbc_ci_rust_sccache_warm_step_timeout_minutes + gunbc_ci_rust_gate_step_timeout_minutes + gunbc_ci_aux_step_timeout_minutes + gunbc_ci_aux_step_timeout_minutes + gunbc_ci_prelude_allowance_minutes } +data gunbc_ci_job_timeout_policy_disposition: Disposition = Terminal { + reason: "gunbc CI budget semantics (operator ruling 2026-07-06): floor step gunbc_ci_floor_step_timeout_minutes (10m) enforced at STEP level (RED-by-failure); ci job backstop gunbc_ci_job_backstop_timeout_minutes() = release build + floor + aux + prelude (65m) so cold release build is not killed by a job cap shorter than gunbc_ci_release_build_step_timeout_minutes (receipt run 28819559597 @ 201371c: 10m job kill aborted cargo mid-build). rust_tests keeps step-sum job backstop gunbc_ci_rust_job_backstop_timeout_minutes() = warm + gate + aux + prelude (40m at current step carriers: 15+10+5+5+5) โ€” job cap must cover step budgets so gates reach RED-by-step-failure, not job-cancelled silent-pass (#6323 starvation-kill scoped to ci/floor only on main). dissolve-on: compile-clean scoping/memoization shrinks floor under measured envelope." +} + fn ci_deploy_job(stage: DeployStage) -> Job { Job { id: stage.id, @@ -381,7 +383,7 @@ fn ci_job() -> Job { outputs: none, if_condition: none, strategy: none, - timeout_minutes: Present { value: gunbc_ci_floor_job_runner_starvation_kill_minutes }, + timeout_minutes: Present { value: gunbc_ci_job_backstop_timeout_minutes() }, continue_on_error: none, concurrency: none } diff --git a/dag/std/interface_summary.dag b/dag/std/interface_summary.dag new file mode 100644 index 00000000000..52c762765cd --- /dev/null +++ b/dag/std/interface_summary.dag @@ -0,0 +1,96 @@ +module std.interface_summary + +import std.types { ContentHash, NonEmptyStr, List } +import std.content_hash { content_hash_atom, content_hash_combine, content_hash_tagged } + +type InterfaceHash = ContentHash where brand("InterfaceHash") +type ModuleKey = ContentHash where brand("ModuleKey") +type SignatureFingerprint = ContentHash where brand("SignatureFingerprint") + +type InterfaceContract + = ContractAbsent + | SignatureContract { signature: SignatureFingerprint } + +type ExportKind + = ExportFn + | ExportType + | ExportData + | ExportService + +type ExportEntry { + name: NonEmptyStr + kind: ExportKind + contract: InterfaceContract +} + +type InterfaceSummary { + module_path: NonEmptyStr + exports: List + interface_hash: InterfaceHash +} + +data interface_summary_v0_dissolution_trigger: String = "dissolve-on: semantic contract slot (FLAG D) populates per-seam as P formulas ground; signature-only v0 is the interim carrier. Per-declaration export grain + module rollup hash (FLAG B)." + +fn signature_contract(signature: SignatureFingerprint) -> InterfaceContract { + SignatureContract { signature: signature } +} + +fn contract_absent() -> InterfaceContract { + ContractAbsent +} + +fn export_kind_tag(kind: ExportKind) -> NonEmptyStr { + match kind { + ExportFn => "fn" + ExportType => "type" + ExportData => "data" + ExportService => "service" + } +} + +fn export_entry_fingerprint(entry: ExportEntry) -> ContentHash { + match entry.contract { + ContractAbsent => + content_hash_tagged( + tag: "export", + payload: content_hash_combine( + left: content_hash_atom(value: entry.name), + right: content_hash_atom(value: export_kind_tag(kind: entry.kind)) + ) + ) + SignatureContract { signature: signature } => + content_hash_tagged( + tag: "export", + payload: content_hash_combine( + left: content_hash_combine( + left: content_hash_atom(value: entry.name), + right: content_hash_atom(value: export_kind_tag(kind: entry.kind)) + ), + right: signature + ) + ) + } +} + +fn interface_summary_rollup(exports: List) -> InterfaceHash { + fold( + exports, + init: content_hash_atom(value: "interface-summary-v0"), + f: fn(acc, entry) { + content_hash_combine(left: acc, right: export_entry_fingerprint(entry: entry)) + } + ) +} + +fn module_key(source_hash: ContentHash, direct_import_interface_hashes: List) -> ModuleKey { + fold( + direct_import_interface_hashes, + init: content_hash_tagged(tag: "module-key-source", payload: source_hash), + f: fn(acc, import_hash) { + content_hash_tagged( + tag: "module-key-import", + payload: content_hash_combine(left: acc, right: import_hash) + ) + } + ) +} diff --git a/dag/test/claim/ci_yaml_serializer_witness_test.dag b/dag/test/claim/ci_yaml_serializer_witness_test.dag index 6ffd9b9d0c2..624871c9049 100644 --- a/dag/test/claim/ci_yaml_serializer_witness_test.dag +++ b/dag/test/claim/ci_yaml_serializer_witness_test.dag @@ -221,7 +221,7 @@ fn witness_warm_step_budgeted_separately_from_gate() -> Bool { fn witness_rust_tests_yaml_has_observable_warm_step_before_gate() -> Bool { let yaml = expected_ci_yml() string_contains(s: yaml, pattern: "sccache warm (release bins + cache prime)") && - string_contains(s: yaml, pattern: "timeout-minutes: 45\n - name: v1 rust gate (fmt + clippy + nextest)") && + string_contains(s: yaml, pattern: "timeout-minutes: 46\n - name: v1 rust gate (fmt + clippy + nextest)") && string_contains(s: yaml, pattern: "dag/tools/rust_gates_ci.dag") && !string_contains( s: yaml, diff --git a/src/v1/04_method.dag b/src/v1/04_method.dag index b0e195cd7f9..ab3e2daa113 100644 --- a/src/v1/04_method.dag +++ b/src/v1/04_method.dag @@ -140,6 +140,7 @@ fn builtin_function_registry() -> Map { let m = map_insert(m, "module_declaration_facts", list_of_type_variable(id: "module_declaration_fact_elem")) let m = map_insert(m, "concept_decl_facts", list_of_type_variable(id: "concept_decl_elem")) let m = map_insert(m, "decl_facts", list_of_type_variable(id: "decl_fact_elem")) + let m = map_insert(m, "export_signature_facts", list_of_type_variable(id: "decl_fact_elem")) let m = map_insert(m, "medium_structure_leak_facts", list_of_type_variable(id: "medium_structure_leak_fact_elem")) let m = map_insert(m, "fact_cardinality_decl_facts", list_of_type_variable(id: "fact_cardinality_decl_fact_elem")) let m = map_insert(m, "languages_consumer_census_data_decl_count", int_type) diff --git a/src/v1/stage0/src/coproduct_reflection.rs b/src/v1/stage0/src/coproduct_reflection.rs index 2fd2aeb477a..af5a0b5cdbe 100644 --- a/src/v1/stage0/src/coproduct_reflection.rs +++ b/src/v1/stage0/src/coproduct_reflection.rs @@ -13,7 +13,7 @@ use crate::v1_interpreter::{ }; use crate::v1_std_core::{ authored_name_at, expr_var_name_at, field_node_type_expr, inferred_to_node, param_node_name_at, - source_text_at, Connective, ExprData, NewlineIndex, Node, VarBindingKind, + param_node_type_expr, source_text_at, Connective, ExprData, NewlineIndex, Node, VarBindingKind, }; type SourceIndices = Rc>>; @@ -1045,6 +1045,88 @@ fn marshal_decl_fact_node( } } +// SCAFFOLD (ยง7 hand-Rust shrink-to-zero): see +// `v2.std.decl_index::export_signature_facts_host_scaffold_dissolution_trigger`. +// Host SOURCE half for `export_signature_facts`; strict sibling of `marshal_decl_fact_node` / +// `eval_decl_facts`. Reuses `decl_facts_for_roots` (#5966 neat-fox-279) โ€” no new corpus walk. +// Dissolves per `dag/std/interface_summary.dag::interface_summary_v0_dissolution_trigger`. +fn marshal_fn_export_signature_node( + ctx: &InterpContext, + si: &Rc>>, + item: &Rc, +) -> InterpResult { + let mut edges = Vec::new(); + for p in item.params.iter() { + if param_is_type_param(p, si) { + continue; + } + let ty = param_node_type_expr(p.clone()); + edges.push(edge_positional(ctx, marshal_type_expr_ref(ctx, si, &ty)?)); + } + let ret_val = item + .inferred + .as_ref() + .and_then(|inf| inferred_to_node(inf.clone())) + .map(|ret| marshal_type_expr_ref(ctx, si, &ret)) + .transpose()? + .unwrap_or_else(|| unit_type_node(ctx)); + edges.push(edge_positional(ctx, ret_val)); + Ok(node_record( + ctx, + node_kind_type_node(ctx, nullary_connective_variant(ctx, "Arrow")), + edges, + )) +} + +fn marshal_export_signature_node( + ctx: &InterpContext, + item: &Rc, + kind: ItemKind, + si: &SourceIndices, +) -> InterpResult { + match kind { + ItemKind::TypeItem => concept_decl_node(ctx, si, item), + ItemKind::FnItem | ItemKind::FuncItem => marshal_fn_export_signature_node(ctx, si, item), + ItemKind::DataItem => match item + .inferred + .as_ref() + .and_then(|inf| inferred_to_node(inf.clone())) + { + Some(ty) => marshal_type_expr_ref(ctx, si, &ty), + None => Ok(unit_type_node(ctx)), + }, + _ => Ok(unit_type_node(ctx)), + } +} + +pub fn eval_export_signature_facts( + ctx: &InterpContext, + pool_roots: &[String], +) -> InterpResult { + let facts = decl_facts_for_roots(pool_roots); + let mut rows = Vec::with_capacity(facts.len()); + for fact in facts { + let node = marshal_export_signature_node(ctx, &fact.node, fact.kind, &fact.source_indices) + .map_err(|e| InterpError::TypeError { + msg: format!( + "export_signature_facts: failed to marshal `{}` ({:?}) in `{}`: {e}", + fact.qualified_name, fact.kind, fact.rel_path + ), + })?; + rows.push(Value::Record { + type_name: ctx.sym("DeclFact"), + fields: Rc::new(sorted_fields(vec![ + (ctx.sym("qualified_name"), Value::Str(fact.qualified_name)), + (ctx.sym("name"), Value::Str(fact.name)), + (ctx.sym("kind"), marshal_decl_item_kind(ctx, fact.kind)), + (ctx.sym("node"), node), + (ctx.sym("rel_path"), Value::Str(fact.rel_path)), + ])), + }); + } + Ok(crate::v1_interpreter::list_value(rows)) +} + pub fn eval_decl_facts(ctx: &InterpContext, pool_roots: &[String]) -> InterpResult { let facts = decl_facts_for_roots(pool_roots); let mut rows = Vec::with_capacity(facts.len()); diff --git a/src/v1/stage0/src/v1_compiler_infer_method.rs b/src/v1/stage0/src/v1_compiler_infer_method.rs index 234c4dfff8f..aeb25e3d892 100644 --- a/src/v1/stage0/src/v1_compiler_infer_method.rs +++ b/src/v1/stage0/src/v1_compiler_infer_method.rs @@ -393,6 +393,11 @@ pub fn builtin_function_registry() -> Rc>> { "decl_facts".to_string(), list_of_type_variable("decl_fact_elem".to_string()), ); + let m = v1_rt::rc_map_insert( + m.clone(), + "export_signature_facts".to_string(), + list_of_type_variable("decl_fact_elem".to_string()), + ); let m = v1_rt::rc_map_insert( m.clone(), "medium_structure_leak_facts".to_string(), diff --git a/src/v1/stage0/src/v1_interpreter.rs b/src/v1/stage0/src/v1_interpreter.rs index 610b7f91c32..eff252ab217 100644 --- a/src/v1/stage0/src/v1_interpreter.rs +++ b/src/v1/stage0/src/v1_interpreter.rs @@ -5728,6 +5728,14 @@ fn eval_builtin( )?)) } + "export_signature_facts" => { + let pool_roots = + expect_str_list(positional.first().copied(), "export_signature_facts")?; + Ok(Some( + crate::coproduct_reflection::eval_export_signature_facts(ctx, &pool_roots)?, + )) + } + "decl_facts" => { let pool_roots = expect_str_list(positional.first().copied(), "decl_facts")?; Ok(Some(crate::coproduct_reflection::eval_decl_facts( diff --git a/src/v2/compiler/03_body_producer.dag b/src/v2/compiler/03_body_producer.dag index 9c83bd50cdd..6db80b201cd 100644 --- a/src/v2/compiler/03_body_producer.dag +++ b/src/v2/compiler/03_body_producer.dag @@ -1,7 +1,7 @@ module v2.compiler.body_producer -import v2.std.algebra { fold_list, list_append, list_snoc_item } -import v2.std.collection { Absent, List, Optional, Present, list_at_optional } +import v2.std.algebra { list_append, list_snoc_item } +import v2.std.collection { Absent, List, Optional, Present, list_at_optional, optional_absent } import v2.std.diagnostic { Accepted, Diagnostic, @@ -255,20 +255,16 @@ fn mvp1_resolved_first_atom_with_binding( reason: Symbol, witness: Node ) -> Outcome { - match fold_list( - xs: atoms, - empty: Absent, - cons: fn(found, atom) { - match found { - Present { value: _ } => found - Absent => - match mvp1_atom_has_binding_identity(node: atom, wanted_binding: wanted_binding) { - true => Present { value: atom } - false => Absent - } - } + match fold(atoms, init: optional_absent(), f: fn(found, atom) { + match found { + Present { value: _ } => found + Absent => + match mvp1_atom_has_binding_identity(node: atom, wanted_binding: wanted_binding) { + true => Present { value: atom } + false => optional_absent() + } } - ) { + }) { Present { value: node } => outcome_accepted(value: node) Absent => outcome_rejected( diff --git a/src/v2/compiler/manual/dag_import_block_lexeme_stamp_test.dag b/src/v2/compiler/manual/dag_import_block_lexeme_stamp_test.dag index b19517e26a9..5c69856f372 100644 --- a/src/v2/compiler/manual/dag_import_block_lexeme_stamp_test.dag +++ b/src/v2/compiler/manual/dag_import_block_lexeme_stamp_test.dag @@ -7,7 +7,7 @@ import v2.extdeps.languages.dag { parse_import_block_idents, parse_production_emitted_identity_optional } -import v2.std.collection { Absent, List, Optional, Present, optional_present } +import v2.std.collection { Absent, List, Optional, Present, optional_absent, optional_present } import v2.std.diagnostic { Accepted, Diagnostic, @@ -40,16 +40,12 @@ fn dag_lex_stamp_parse_tree() -> Outcome { } fn dag_lex_stamp_find_import_node_in_children(tree: Node) -> Optional { - fold( - tree.children, - init: Absent, - f: fn(acc, edge) { - match acc { - Present { value: _ } => acc - Absent => dag_lex_stamp_find_import_node(tree: edge.target) - } + fold(tree.children, init: optional_absent(), f: fn(acc, edge) { + match acc { + Present { value: _ } => acc + Absent => dag_lex_stamp_find_import_node(tree: edge.target) } - ) + }) } fn dag_lex_stamp_find_import_node(tree: Node) -> Optional { diff --git a/src/v2/compiler/manual/typescript_import_pipeline_test.dag b/src/v2/compiler/manual/typescript_import_pipeline_test.dag index 83807292d08..6bf6a03073d 100644 --- a/src/v2/compiler/manual/typescript_import_pipeline_test.dag +++ b/src/v2/compiler/manual/typescript_import_pipeline_test.dag @@ -15,7 +15,7 @@ import v2.extdeps.languages.typescript { ts_mvp1_target_model_node, ts_wave2a_lex } -import v2.std.collection { Absent, Map, Optional, Present, empty_map, map_insert, optional_present } +import v2.std.collection { Absent, Map, Optional, Present, empty_map, map_insert, optional_absent, optional_present } import v2.std.compilers.target_model { TargetModel, target_model_emit_transforms_empty } import v2.std.diagnostic { Accepted, @@ -74,16 +74,12 @@ fn ts_import_pipeline_target(third_seg: Symbol, third_spell: String) -> TargetMo } fn ts_import_pipeline_find_import_node_children(tree: Node) -> Optional { - fold( - tree.children, - init: Absent, - f: fn(acc, edge) { - match acc { - Present { value: _ } => acc - Absent => ts_import_pipeline_find_import_node(tree: edge.target) - } + fold(tree.children, init: optional_absent(), f: fn(acc, edge) { + match acc { + Present { value: _ } => acc + Absent => ts_import_pipeline_find_import_node(tree: edge.target) } - ) + }) } fn ts_import_pipeline_find_import_node(tree: Node) -> Optional { diff --git a/src/v2/lens/interface_summary.dag b/src/v2/lens/interface_summary.dag new file mode 100644 index 00000000000..02799ece06e --- /dev/null +++ b/src/v2/lens/interface_summary.dag @@ -0,0 +1,219 @@ +module v2.lens.interface_summary + +import v2.lens.common.construction_justification { ConstructionJustification, WallAfterGrounding } +import std.disposition { SingleAuthority } +import std.content_hash { content_hash_atom } +import std.interface_summary { + ExportData, + ExportEntry, + ExportFn, + ExportKind, + ExportService, + ExportType, + InterfaceHash, + InterfaceSummary, + ModuleKey, + SignatureFingerprint, + interface_summary_rollup, + module_key, + signature_contract +} +import std.types { ContentHash, NonEmptyStr, List, String } +import v2.lens.module_graph { + ImportResolutionFact, + ModuleDeclarationFact, + import_resolution_facts_live, + module_declaration_facts_live +} +import v2.std.decl_index { + DeclFact, + ItemKind, + DataItem, + FnItem, + FuncItem, + ServiceItem, + TypeItem, + export_signature_facts +} +import v2.std.node { Node, content_hash } +import v2.std.algebra { Empty, list_snoc_item } +import v2.std.collection { Present, Absent, optional_absent } + +fn decl_kind_to_export_kind(kind: ItemKind) -> ExportKind? { + match kind { + FnItem => Present { value: ExportFn } + FuncItem => Present { value: ExportFn } + TypeItem => Present { value: ExportType } + DataItem => Present { value: ExportData } + ServiceItem => Present { value: ExportService } + OtherItem => Absent + } +} + +fn signature_fingerprint_of_node(node: Node) -> SignatureFingerprint { + content_hash_atom(value: content_hash(n: node)) +} + +fn export_entry_from_decl_fact(fact: DeclFact) -> ExportEntry? { + match decl_kind_to_export_kind(kind: fact.kind) { + Present { value: kind } => + Present { + value: ExportEntry { + name: fact.name, + kind: kind, + contract: signature_contract( + signature: signature_fingerprint_of_node(node: fact.node) + ) + } + } + Absent => Absent + } +} + +fn export_entries_for_rel_path(rel_path: String, facts: List) -> List { + fold(facts, init: Empty, f: fn(acc, fact) { + if fact.rel_path == rel_path { + match export_entry_from_decl_fact(fact: fact) { + Present { value: entry } => list_snoc_item(xs: acc, item: entry) + Absent => acc + } + } else { + acc + } + }) +} + +fn interface_summary_for_module_path( + module_path: NonEmptyStr, + rel_path: String, + facts: List +) -> InterfaceSummary { + let exports = export_entries_for_rel_path(rel_path: rel_path, facts: facts) + InterfaceSummary { + module_path: module_path, + exports: exports, + interface_hash: interface_summary_rollup(exports: exports) + } +} + +fn interface_summaries_from_facts( + modules: List, + facts: List +) -> List { + fold(modules, init: Empty, f: fn(acc, mod_fact) { + list_snoc_item( + xs: acc, + item: interface_summary_for_module_path( + module_path: mod_fact.module, + rel_path: mod_fact.path, + facts: facts + ) + ) + }) +} + +fn interface_summaries_live(pool_roots: List) -> List { + interface_summaries_from_facts( + modules: module_declaration_facts_live(pool_roots: pool_roots), + facts: export_signature_facts(pool_roots: pool_roots) + ) +} + +fn interface_hash_for_module_path( + summaries: List, + module_path: String +) -> InterfaceHash? { + fold(summaries, init: optional_absent(), f: fn(found, summary) { + match found { + Present { value: _ } => found + Absent => + match summary.module_path == module_path { + true => Present { value: summary.interface_hash } + false => found + } + } + }) +} + +fn direct_import_modules( + rel_path: String, + edges: List +) -> List { + fold(edges, init: Empty, f: fn(acc, edge) { + if edge.path == rel_path { + list_snoc_item(xs: acc, item: edge.import_module) + } else { + acc + } + }) +} + +fn import_interface_hashes_for_module( + rel_path: String, + edges: List, + summaries: List +) -> List? { + fold( + direct_import_modules(rel_path: rel_path, edges: edges), + init: Present { value: Empty }, + f: fn(acc, import_module) { + match acc { + Absent => Absent + Present { value: hashes } => + match interface_hash_for_module_path(summaries: summaries, module_path: import_module) { + Present { value: hash } => + Present { value: list_snoc_item(xs: hashes, item: hash) } + Absent => Absent + } + } + } + ) +} + +fn module_key_for_rel_path( + rel_path: String, + module_path: NonEmptyStr, + source_hash: ContentHash, + pool_roots: List +) -> ModuleKey? { + let summaries = interface_summaries_live(pool_roots: pool_roots) + let edges = import_resolution_facts_live( + pool_roots: pool_roots, + importer_roots: pool_roots, + exclude_substrings: [] + ) + match import_interface_hashes_for_module( + rel_path: rel_path, + edges: edges, + summaries: summaries + ) { + Present { value: import_hashes } => + Present { + value: module_key( + source_hash: source_hash, + direct_import_interface_hashes: import_hashes + ) + } + Absent => Absent + } +} + +fn interface_summary_for_module( + module_path: NonEmptyStr, + pool_roots: List +) -> InterfaceSummary? { + fold(interface_summaries_live(pool_roots: pool_roots), init: optional_absent(), f: fn(found, summary) { + match found { + Present { value: _ } => found + Absent => + match summary.module_path == module_path { + true => Present { value: summary } + false => found + } + } + }) +} + +data construction_justification: ConstructionJustification = ConstructionJustification { + class: WallAfterGrounding { dissolves_to: SingleAuthority } +} diff --git a/src/v2/lens/unit_modeling.dag b/src/v2/lens/unit_modeling.dag index 1b54952c86c..f140e80eeb9 100644 --- a/src/v2/lens/unit_modeling.dag +++ b/src/v2/lens/unit_modeling.dag @@ -136,7 +136,7 @@ fn field_unit_violation(carrier: Node, edge: Edge) -> Optional { fn carrier_first_unit_violation(carrier: Node) -> Optional { match carrier.kind { TypeNode { connective: _ } => - fold(carrier.children, init: Absent, f: fn(acc, edge) { + fold(carrier.children, init: optional_absent(), f: fn(acc, edge) { optional_prefer_first_present(left: acc, right: field_unit_violation(carrier: carrier, edge: edge)) }) ComputationNode { behavior: _ } => Absent @@ -144,7 +144,7 @@ fn carrier_first_unit_violation(carrier: Node) -> Optional { } fn subtree_first_unit_violation(node: Node) -> Optional { - fold(node_subtree_nodes(root: node), init: Absent, f: fn(acc, n) { + fold(node_subtree_nodes(root: node), init: optional_absent(), f: fn(acc, n) { optional_prefer_first_present(left: acc, right: carrier_first_unit_violation(carrier: n)) }) } diff --git a/src/v2/std/compilers/target_model.dag b/src/v2/std/compilers/target_model.dag index 4b404b1747c..8f2b75ad52e 100644 --- a/src/v2/std/compilers/target_model.dag +++ b/src/v2/std/compilers/target_model.dag @@ -7137,17 +7137,13 @@ fn target_atom_realization_lookup_extract_row( catalog: List, source_carrier: Node ) -> Optional { - fold_list( - xs: catalog, - empty: optional_absent(), - cons: fn(acc, item) { - target_atom_realization_lookup_extract_row_step( - acc: acc, - row: item, - source_carrier: source_carrier - ) - } - ) + fold(catalog, init: optional_absent(), f: fn(acc, item) { + target_atom_realization_lookup_extract_row_step( + acc: acc, + row: item, + source_carrier: source_carrier + ) + }) } fn target_atom_realization_lookup_in_catalog( @@ -9027,21 +9023,17 @@ fn target_collection_select_choice( if target_collection_choice_constraints_satisfied(choice: primary, elem: elem) { optional_present(value: primary) } else { - fold_list( - xs: alternatives, - empty: optional_absent(), - cons: fn(acc, choice) { - match acc { - Present { value: _ } => acc - Absent => - if target_collection_choice_constraints_satisfied(choice: choice, elem: elem) { - optional_present(value: choice) - } else { - optional_absent() - } - } + fold(alternatives, init: optional_absent(), f: fn(acc, choice) { + match acc { + Present { value: _ } => acc + Absent => + if target_collection_choice_constraints_satisfied(choice: choice, elem: elem) { + optional_present(value: choice) + } else { + optional_absent() + } } - ) + }) } } diff --git a/src/v2/std/decl_index.dag b/src/v2/std/decl_index.dag index c00f05198cd..166f61abf24 100644 --- a/src/v2/std/decl_index.dag +++ b/src/v2/std/decl_index.dag @@ -27,6 +27,12 @@ fn decl_facts(pool_roots: List) -> List { decl_facts(pool_roots: pool_roots) } +fn export_signature_facts(pool_roots: List) -> List { + export_signature_facts(pool_roots: pool_roots) +} + +data export_signature_facts_host_scaffold_dissolution_trigger: String = "SCAFFOLD (ยง7 hand-Rust shrink-to-zero): stage0 SOURCE projection in src/v1/stage0/src/coproduct_reflection.rs (`marshal_fn_export_signature_node`, `eval_export_signature_facts`). Strict sibling of `decl_facts` / `marshal_decl_fact_node` โ€” reuses `decl_facts_for_roots` (#5966 neat-fox-279 corpus walker); no second whole-tree walk authority. DISSOLVES WHEN `dag/std/interface_summary.dag::interface_summary_v0_dissolution_trigger` (v2 native signature export, FLAG D). Census: deletes with the #5966 decl_facts host-builtin family, not as an independent Rust island." + fn logical_qualified_name_from_module(module_name: String, name: String) -> String { let start = if module_name.starts_with(prefix: "v2.") { 3 } else { 0 } let logical = module_name.substring(start: start, end: module_name.length()) diff --git a/src/v2/test/claim/interface_summary/interface_summary_firewall_test.dag b/src/v2/test/claim/interface_summary/interface_summary_firewall_test.dag new file mode 100644 index 00000000000..e000b52c5da --- /dev/null +++ b/src/v2/test/claim/interface_summary/interface_summary_firewall_test.dag @@ -0,0 +1,136 @@ +module v2.test.claim.interface_summary.interface_summary_firewall + +import std.interface_summary { export_entry_fingerprint } +import std.types { ContentHash, NonEmptyStr, List } +import v2.lens.interface_summary { + interface_summary_for_module, + import_interface_hashes_for_module +} +import v2.lens.module_graph { ImportResolutionFact } +import v2.std.algebra { Empty } +import v2.std.collection { Present, Absent, optional_absent } +import v2.std.decl_index { export_signature_facts_host_scaffold_dissolution_trigger } +import v2.std.logic { Bool } + +data interface_summary_firewall_host_scaffold_note: String = "Host-scaffold witness: reads the live tree via export_signature_facts and module_declaration_facts_live host intrinsics (_facts_live( pattern); floor:host_scaffold โ€” never node-skipped on the CI floor." + +data firewall_fixture_roots: List = ["src/v2/test/fixture/interface_summary"] + +fn export_fingerprint_for_gate(module_path: NonEmptyStr) -> ContentHash? { + match interface_summary_for_module( + module_path: module_path, + pool_roots: firewall_fixture_roots + ) { + Present { value: summary } => + fold(summary.exports, init: optional_absent(), f: fn(found, entry) { + match found { + Present { value: _ } => found + Absent => + match entry.name == "firewall_gate" { + true => Present { value: export_entry_fingerprint(entry: entry) } + false => found + } + } + }) + Absent => Absent + } +} + +fn firewall_body_v1_fingerprint_receipt() -> String { + match export_fingerprint_for_gate( + module_path: "v2.test.fixture.interface_summary.firewall_body_v1" + ) { + Present { value: fp } => fp + Absent => "absent" + } +} + +fn firewall_body_v2_fingerprint_receipt() -> String { + match export_fingerprint_for_gate( + module_path: "v2.test.fixture.interface_summary.firewall_body_v2" + ) { + Present { value: fp } => fp + Absent => "absent" + } +} + +fn firewall_sig_variant_fingerprint_receipt() -> String { + match export_fingerprint_for_gate( + module_path: "v2.test.fixture.interface_summary.firewall_sig_variant" + ) { + Present { value: fp } => fp + Absent => "absent" + } +} + +fn body_only_edit_preserves_export_fingerprint() -> Bool { + let v1 = export_fingerprint_for_gate( + module_path: "v2.test.fixture.interface_summary.firewall_body_v1" + ) + let v2 = export_fingerprint_for_gate( + module_path: "v2.test.fixture.interface_summary.firewall_body_v2" + ) + match v1 { + Present { value: fp1 } => + match v2 { + Present { value: fp2 } => fp1 == fp2 + Absent => false + } + Absent => false + } +} + +fn signature_edit_changes_export_fingerprint() -> Bool { + let baseline = export_fingerprint_for_gate( + module_path: "v2.test.fixture.interface_summary.firewall_body_v1" + ) + let variant = export_fingerprint_for_gate( + module_path: "v2.test.fixture.interface_summary.firewall_sig_variant" + ) + match baseline { + Present { value: fp1 } => + match variant { + Present { value: fp2 } => fp1 != fp2 + Absent => false + } + Absent => false + } +} + +test fn interface_summary_firewall_body_only_edit_is_green() -> Bool { + body_only_edit_preserves_export_fingerprint() +} + +test fn interface_summary_firewall_signature_edit_is_red() -> Bool { + signature_edit_changes_export_fingerprint() +} + +test fn export_signature_facts_host_scaffold_receipt_cites_dissolution_authorities() -> Bool { + string_contains(s: export_signature_facts_host_scaffold_dissolution_trigger, pattern: "#5966") + && string_contains( + s: export_signature_facts_host_scaffold_dissolution_trigger, + pattern: "interface_summary_v0_dissolution_trigger" + ) + && string_contains( + s: export_signature_facts_host_scaffold_dissolution_trigger, + pattern: "decl_facts_for_roots" + ) +} + +test fn interface_summary_import_unresolved_refuses_module_key_hashes() -> Bool { + let edges = [ + ImportResolutionFact { + path: "src/mod.dag", + import_module: "missing.other", + target_declared: true + } + ] + match import_interface_hashes_for_module( + rel_path: "src/mod.dag", + edges: edges, + summaries: Empty + ) { + Absent => true + Present { value: _ } => false + } +} diff --git a/src/v2/test/fixture/interface_summary/firewall_body_v1.dag b/src/v2/test/fixture/interface_summary/firewall_body_v1.dag new file mode 100644 index 00000000000..058777a6119 --- /dev/null +++ b/src/v2/test/fixture/interface_summary/firewall_body_v1.dag @@ -0,0 +1,7 @@ +module v2.test.fixture.interface_summary.firewall_body_v1 + +import v2.std.integer { Int } + +fn firewall_gate(x: Int) -> Int { + x + 1 +} diff --git a/src/v2/test/fixture/interface_summary/firewall_body_v2.dag b/src/v2/test/fixture/interface_summary/firewall_body_v2.dag new file mode 100644 index 00000000000..12bd056f403 --- /dev/null +++ b/src/v2/test/fixture/interface_summary/firewall_body_v2.dag @@ -0,0 +1,7 @@ +module v2.test.fixture.interface_summary.firewall_body_v2 + +import v2.std.integer { Int } + +fn firewall_gate(x: Int) -> Int { + x + 2 +} diff --git a/src/v2/test/fixture/interface_summary/firewall_sig_variant.dag b/src/v2/test/fixture/interface_summary/firewall_sig_variant.dag new file mode 100644 index 00000000000..ea05a38b50b --- /dev/null +++ b/src/v2/test/fixture/interface_summary/firewall_sig_variant.dag @@ -0,0 +1,8 @@ +module v2.test.fixture.interface_summary.firewall_sig_variant + +import v2.std.integer { Int } +import v2.std.logic { Bool } + +fn firewall_gate(x: Int) -> Bool { + true +}